From d9298901db25bafb39ca72118a24aab78703cd27 Mon Sep 17 00:00:00 2001 From: DasIch Date: Thu, 29 Apr 2010 18:06:22 +0200 Subject: Show python 3.x incompatibilities which cannot be trivially fixed --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7057a715..682f0366 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PYTHON ?= python +PYTHON ?= python -3 export PYTHONPATH = $(shell echo "$$PYTHONPATH"):./sphinx -- cgit v1.2.1 From 8de89e0ac2c2131d27cea47df5fa7be326cab613 Mon Sep 17 00:00:00 2001 From: DasIch Date: Thu, 29 Apr 2010 18:08:44 +0200 Subject: Replace .has_key() calls with the in-operator --- sphinx/writers/latex.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 5674b388..360cf40c 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -314,7 +314,7 @@ class LaTeXTranslator(nodes.NodeVisitor): # ... and all others are the appendices self.body.append(u'\n\\appendix\n') self.first_document = -1 - if node.has_key('docname'): + if 'docname' in node: self.body.append(self.hypertarget(':doc')) # "- 1" because the level is increased before the title is visited self.sectionlevel = self.top_sectionlevel - 1 @@ -694,7 +694,7 @@ class LaTeXTranslator(nodes.NodeVisitor): self.table.rowcount += 1 def visit_entry(self, node): - if node.has_key('morerows') or node.has_key('morecols'): + if 'morerows' in node or 'morecols' in node: raise UnsupportedError('%s:%s: column or row spanning cells are ' 'not yet implemented.' % (self.curfilestack[-1], node.line or '')) @@ -751,7 +751,7 @@ class LaTeXTranslator(nodes.NodeVisitor): def visit_term(self, node): ctx = '}] \\leavevmode' - if node.has_key('ids') and node['ids']: + if node.get('ids'): ctx += self.hypertarget(node['ids'][0]) self.body.append('\\item[{') self.context.append(ctx) @@ -833,20 +833,20 @@ class LaTeXTranslator(nodes.NodeVisitor): post = [] include_graphics_options = [] is_inline = self.is_inline(node) - if attrs.has_key('scale'): + if 'scale' in attrs: # Could also be done with ``scale`` option to # ``\includegraphics``; doing it this way for consistency. pre.append('\\scalebox{%f}{' % (attrs['scale'] / 100.0,)) post.append('}') - if attrs.has_key('width'): + if 'width' in attrs: w = self.latex_image_length(attrs['width']) if w: include_graphics_options.append('width=%s' % w) - if attrs.has_key('height'): + if 'height' in attrs: h = self.latex_image_length(attrs['height']) if h: include_graphics_options.append('height=%s' % h) - if attrs.has_key('align'): + if 'align' in attrs: align_prepost = { # By default latex aligns the top of an image. (1, 'top'): ('', ''), @@ -887,13 +887,13 @@ class LaTeXTranslator(nodes.NodeVisitor): pass def visit_figure(self, node): - if node.has_key('width') and node.get('align', '') in ('left', 'right'): + if 'width' in node and node.get('align', '') in ('left', 'right'): self.body.append('\\begin{wrapfigure}{%s}{%s}\n\\centering' % (node['align'] == 'right' and 'r' or 'l', node['width'])) self.context.append('\\end{wrapfigure}\n') else: - if (not node.attributes.has_key('align') or + if (not 'align' in node.attributes or node.attributes['align'] == 'center'): # centering does not add vertical space like center. align = '\n\\centering' @@ -1154,7 +1154,7 @@ class LaTeXTranslator(nodes.NodeVisitor): self.no_contractions -= 1 if self.in_title: self.body.append(r'\texttt{%s}' % content) - elif node.has_key('role') and node['role'] == 'samp': + elif node.get('role') == 'samp': self.body.append(r'\samp{%s}' % content) else: self.body.append(r'\code{%s}' % content) @@ -1183,10 +1183,10 @@ class LaTeXTranslator(nodes.NodeVisitor): code = self.verbatim.rstrip('\n') lang = self.hlsettingstack[-1][0] linenos = code.count('\n') >= self.hlsettingstack[-1][1] - 1 - if node.has_key('language'): + if 'language' in node: # code-block directives lang = node['language'] - if node.has_key('linenos'): + if 'linenos' in node: linenos = node['linenos'] hlcode = self.highlighter.highlight_block(code, lang, linenos) # workaround for Unicode issue -- cgit v1.2.1 From 15aff2ffba78ef0d221ac714c52f4f1d3a0a2ad2 Mon Sep 17 00:00:00 2001 From: DasIch Date: Thu, 29 Apr 2010 19:42:17 +0200 Subject: Removed map(None, ...) usage --- sphinx/writers/text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py index 98528d5b..b28b2379 100644 --- a/sphinx/writers/text.py +++ b/sphinx/writers/text.py @@ -390,7 +390,7 @@ class TextTranslator(nodes.NodeVisitor): self.add_text(''.join(out) + '\n') def writerow(row): - lines = map(None, *row) + lines = zip(*row) for line in lines: out = ['|'] for i, cell in enumerate(line): -- cgit v1.2.1 From c1249b9d9f69ff900882b3573699491bc98ebe5a Mon Sep 17 00:00:00 2001 From: DasIch Date: Thu, 29 Apr 2010 20:34:08 +0200 Subject: Don't use execfile() anymore --- sphinx/config.py | 6 +++++- tests/test_quickstart.py | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/sphinx/config.py b/sphinx/config.py index 12c2a04b..f76d330a 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -165,7 +165,11 @@ class Config(object): try: try: os.chdir(dirname) - execfile(config['__file__'], config) + try: + f = open(config_file, 'U') + exec f in config + finally: + f.close() except SyntaxError, err: raise ConfigError('There is a syntax error in your ' 'configuration file: ' + str(err)) diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index cb40d27c..34c54f95 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -85,7 +85,11 @@ def test_quickstart_defaults(tempdir): conffile = tempdir / 'conf.py' assert conffile.isfile() ns = {} - execfile(conffile, ns) + try: + f = open(conffile, 'U') + exec f in ns + finally: + f.close() assert ns['extensions'] == [] assert ns['templates_path'] == ['_templates'] assert ns['source_suffix'] == '.rst' @@ -138,7 +142,11 @@ def test_quickstart_all_answers(tempdir): conffile = tempdir / 'source' / 'conf.py' assert conffile.isfile() ns = {} - execfile(conffile, ns) + try: + f = open(conffile, 'U') + exec f in ns + finally: + f.close() assert ns['extensions'] == ['sphinx.ext.autodoc', 'sphinx.ext.doctest'] assert ns['templates_path'] == ['.templates'] assert ns['source_suffix'] == '.txt' -- cgit v1.2.1 From 455ebde09dc55d91088d15a89fab0f2dfd1d3b51 Mon Sep 17 00:00:00 2001 From: DasIch Date: Fri, 30 Apr 2010 12:32:42 +0200 Subject: Make sphinx.domains.cpp.DefExpr unhashable --- sphinx/domains/cpp.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 4dac8925..90c3533e 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -132,6 +132,8 @@ class DefExpr(object): def __ne__(self, other): return not self.__eq__(other) + __hash__ = None + def clone(self): """Close a definition expression node""" return deepcopy(self) -- cgit v1.2.1 From a25745aa06d3cdad97988e8795fb2492133a55c8 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sat, 1 May 2010 19:17:52 +0200 Subject: Fixed issue #1 --- sphinx/environment.py | 3 ++- sphinx/pycode/pgen2/tokenize.py | 4 +++- utils/reindent.py | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/sphinx/environment.py b/sphinx/environment.py index 5edcb4d9..fa8460cb 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -1488,8 +1488,9 @@ class BuildEnvironment: i += 1 # group the entries by letter - def keyfunc2((k, v), letters=string.ascii_uppercase + '_'): + def keyfunc2(item, letters=string.ascii_uppercase + '_'): # hack: mutating the subitems dicts to a list in the keyfunc + k, v = item v[1] = sorted((si, se) for (si, (se, void)) in v[1].iteritems()) # now calculate the key letter = k[0].upper() diff --git a/sphinx/pycode/pgen2/tokenize.py b/sphinx/pycode/pgen2/tokenize.py index 4489db89..7ad9f012 100644 --- a/sphinx/pycode/pgen2/tokenize.py +++ b/sphinx/pycode/pgen2/tokenize.py @@ -143,7 +143,9 @@ class TokenError(Exception): pass class StopTokenizing(Exception): pass -def printtoken(type, token, (srow, scol), (erow, ecol), line): # for testing +def printtoken(type, token, scell, ecell, line): # for testing + srow, scol = scell + erow, ecol = ecell print "%d,%d-%d,%d:\t%s\t%s" % \ (srow, scol, erow, ecol, tok_name[type], repr(token)) diff --git a/utils/reindent.py b/utils/reindent.py index c499f671..bcb6b434 100755 --- a/utils/reindent.py +++ b/utils/reindent.py @@ -244,12 +244,13 @@ class Reindenter: return line # Line-eater for tokenize. - def tokeneater(self, type, token, (sline, scol), end, line, + def tokeneater(self, type, token, scell, end, line, INDENT=tokenize.INDENT, DEDENT=tokenize.DEDENT, NEWLINE=tokenize.NEWLINE, COMMENT=tokenize.COMMENT, NL=tokenize.NL): + sline, scol = scell if type == NEWLINE: # A program statement, or ENDMARKER, will eventually follow, -- cgit v1.2.1 From d82fcd12dbf85af674b7f1ae96f83e8f8c5134ff Mon Sep 17 00:00:00 2001 From: DasIch Date: Sat, 1 May 2010 19:18:31 +0200 Subject: Make sphinx.pycode.nodes.BaseNode unhashable --- sphinx/pycode/nodes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sphinx/pycode/nodes.py b/sphinx/pycode/nodes.py index e7184677..fc6eb93a 100644 --- a/sphinx/pycode/nodes.py +++ b/sphinx/pycode/nodes.py @@ -29,6 +29,8 @@ class BaseNode(object): return NotImplemented return not self._eq(other) + __hash__ = None + def get_prev_sibling(self): """Return previous child in parent's children, or None.""" if self.parent is None: -- cgit v1.2.1 From 4ce956559b3786c58754f0bcf5c2498d9750d10a Mon Sep 17 00:00:00 2001 From: DasIch Date: Sat, 1 May 2010 19:19:24 +0200 Subject: Removed pre-2.3 workaround for booleans --- tests/path.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/path.py b/tests/path.py index ceb895f5..20deb048 100644 --- a/tests/path.py +++ b/tests/path.py @@ -56,12 +56,6 @@ try: except AttributeError: pass -# Pre-2.3 workaround for booleans -try: - True, False -except NameError: - True, False = 1, 0 - # Pre-2.3 workaround for basestring. try: basestring -- cgit v1.2.1 From 2a269673211ea132664998037d3c94f8e0b48367 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sat, 1 May 2010 20:26:05 +0200 Subject: Move open() calls out of the try block --- sphinx/config.py | 2 +- tests/test_quickstart.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx/config.py b/sphinx/config.py index f76d330a..c22a5ee7 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -165,8 +165,8 @@ class Config(object): try: try: os.chdir(dirname) + f = open(config_file, 'U') try: - f = open(config_file, 'U') exec f in config finally: f.close() diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index 34c54f95..71ca95a4 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -85,8 +85,8 @@ def test_quickstart_defaults(tempdir): conffile = tempdir / 'conf.py' assert conffile.isfile() ns = {} + f = open(conffile, 'U') try: - f = open(conffile, 'U') exec f in ns finally: f.close() @@ -142,8 +142,8 @@ def test_quickstart_all_answers(tempdir): conffile = tempdir / 'source' / 'conf.py' assert conffile.isfile() ns = {} + f = open(conffile, 'U') try: - f = open(conffile, 'U') exec f in ns finally: f.close() -- cgit v1.2.1 From d3771a017c17bbc63f89cd6be0e321db490df222 Mon Sep 17 00:00:00 2001 From: Daniel Neuh?user Date: Thu, 6 May 2010 16:05:37 +0200 Subject: Added a file containing the changes made during GSoC. --- CHANGES.DasIch | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 CHANGES.DasIch diff --git a/CHANGES.DasIch b/CHANGES.DasIch new file mode 100644 index 00000000..856a5d87 --- /dev/null +++ b/CHANGES.DasIch @@ -0,0 +1,19 @@ +Changes +======= + +This file contains changes made by Daniel Neuhäuser, during the Google Summer +of Code 2010, to port Sphinx to Python 3.x. Changes are ordered descending by +date. + +May 1: - Removed deprecated tuple parameter unpacking. + - Removed a pre-2.3 workaround for booleans because this creates a + deprecation warning for 3.x, in which you can't assign values to + booleans. + - Moved :func:`open()` calls out of the try-blocks, which fixes revision + c577c25bd44b. + +April 30: Made :cls:`sphinx.domains.cpp.DefExpr` unhashable as described by the + documentation because classes in 3.x don't inherit ``__hash__`` if + they implement ``__eq__``. + +April 29: Removed several deprecated function/method calls. -- cgit v1.2.1 From e3819e0971b3036e5cc8a8ececf097782093257d Mon Sep 17 00:00:00 2001 From: Daniel Neuh?user Date: Sat, 8 May 2010 20:34:19 +0200 Subject: Removed ez_setup which doesn't work with python3 and added use_2to3 for distribute --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 183fcceb..2494851f 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ A development egg can be found `here requires = ['Pygments>=0.8', 'Jinja2>=2.2', 'docutils>=0.5'] if sys.version_info < (2, 4): - print 'ERROR: Sphinx requires at least Python 2.4 to run.' + print('ERROR: Sphinx requires at least Python 2.4 to run.') sys.exit(1) if sys.version_info < (2, 5): @@ -198,4 +198,5 @@ setup( }, install_requires=requires, cmdclass=cmdclass, + use_2to3=True, ) -- cgit v1.2.1 From 367154b7e74df6be170c85531f9b13d4034c6a27 Mon Sep 17 00:00:00 2001 From: Daniel Neuh?user Date: Sat, 8 May 2010 20:53:49 +0200 Subject: Check for unicode before trying to decode input from raw_input. Also use codecs.open when writing non-binary files. sphinx-quickstart now works. --- sphinx/quickstart.py | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/sphinx/quickstart.py b/sphinx/quickstart.py index 884caca7..5820996f 100644 --- a/sphinx/quickstart.py +++ b/sphinx/quickstart.py @@ -11,6 +11,7 @@ import sys, os, time from os import path +from codecs import open TERM_ENCODING = getattr(sys.stdin, 'encoding', None) @@ -659,17 +660,20 @@ def do_prompt(d, key, text, default=None, validator=nonempty): x = raw_input(prompt) if default and not x: x = default - if x.decode('ascii', 'replace').encode('ascii', 'replace') != x: - if TERM_ENCODING: - x = x.decode(TERM_ENCODING) - else: - print turquoise('* Note: non-ASCII characters entered ' - 'and terminal encoding unknown -- assuming ' - 'UTF-8 or Latin-1.') - try: - x = x.decode('utf-8') - except UnicodeDecodeError: - x = x.decode('latin1') + # in 3.x raw_input returns a unicode string, those have no decode + # method + if not isinstance(x, unicode): + if x.decode('ascii', 'replace').encode('ascii', 'replace') != x: + if TERM_ENCODING: + x = x.decode(TERM_ENCODING) + else: + print turquoise('* Note: non-ASCII characters entered ' + 'and terminal encoding unknown -- assuming ' + 'UTF-8 or Latin-1.') + try: + x = x.decode('utf-8') + except UnicodeDecodeError: + x = x.decode('latin1') try: x = validator(x) except ValidationError, err: @@ -834,28 +838,28 @@ directly.''' if d['ext_intersphinx']: conf_text += INTERSPHINX_CONFIG - f = open(path.join(srcdir, 'conf.py'), 'w') - f.write(conf_text.encode('utf-8')) + f = open(path.join(srcdir, 'conf.py'), 'w', encoding='utf-8') + f.write(conf_text) f.close() masterfile = path.join(srcdir, d['master'] + d['suffix']) - f = open(masterfile, 'w') - f.write((MASTER_FILE % d).encode('utf-8')) + f = open(masterfile, 'w', encoding='utf-8') + f.write(MASTER_FILE % d) f.close() if d['makefile']: d['rsrcdir'] = d['sep'] and 'source' or '.' d['rbuilddir'] = d['sep'] and 'build' or d['dot'] + 'build' # use binary mode, to avoid writing \r\n on Windows - f = open(path.join(d['path'], 'Makefile'), 'wb') - f.write((MAKEFILE % d).encode('utf-8')) + f = open(path.join(d['path'], 'Makefile'), 'wb', encoding='utf-8') + f.write(MAKEFILE % d) f.close() if d['batchfile']: d['rsrcdir'] = d['sep'] and 'source' or '.' d['rbuilddir'] = d['sep'] and 'build' or d['dot'] + 'build' - f = open(path.join(d['path'], 'make.bat'), 'w') - f.write((BATCHFILE % d).encode('utf-8')) + f = open(path.join(d['path'], 'make.bat'), 'w', encoding='utf-8') + f.write(BATCHFILE % d) f.close() print -- cgit v1.2.1 From 439798e30afa485cb69dbfc7456c1c374ace73eb Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 18:03:42 +0200 Subject: Use codecs.open(). --- sphinx/quickstart.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/sphinx/quickstart.py b/sphinx/quickstart.py index 5820996f..fe2b43a3 100644 --- a/sphinx/quickstart.py +++ b/sphinx/quickstart.py @@ -660,9 +660,8 @@ def do_prompt(d, key, text, default=None, validator=nonempty): x = raw_input(prompt) if default and not x: x = default - # in 3.x raw_input returns a unicode string, those have no decode - # method if not isinstance(x, unicode): + # for Python 2.x, try to get a Unicode string out of it if x.decode('ascii', 'replace').encode('ascii', 'replace') != x: if TERM_ENCODING: x = x.decode(TERM_ENCODING) @@ -838,12 +837,12 @@ directly.''' if d['ext_intersphinx']: conf_text += INTERSPHINX_CONFIG - f = open(path.join(srcdir, 'conf.py'), 'w', encoding='utf-8') + f = codecs.open(path.join(srcdir, 'conf.py'), 'w', encoding='utf-8') f.write(conf_text) f.close() masterfile = path.join(srcdir, d['master'] + d['suffix']) - f = open(masterfile, 'w', encoding='utf-8') + f = codecs.open(masterfile, 'w', encoding='utf-8') f.write(MASTER_FILE % d) f.close() @@ -851,14 +850,14 @@ directly.''' d['rsrcdir'] = d['sep'] and 'source' or '.' d['rbuilddir'] = d['sep'] and 'build' or d['dot'] + 'build' # use binary mode, to avoid writing \r\n on Windows - f = open(path.join(d['path'], 'Makefile'), 'wb', encoding='utf-8') + f = codecs.open(path.join(d['path'], 'Makefile'), 'wb', encoding='utf-8') f.write(MAKEFILE % d) f.close() if d['batchfile']: d['rsrcdir'] = d['sep'] and 'source' or '.' d['rbuilddir'] = d['sep'] and 'build' or d['dot'] + 'build' - f = open(path.join(d['path'], 'make.bat'), 'w', encoding='utf-8') + f = codecs.open(path.join(d['path'], 'make.bat'), 'w', encoding='utf-8') f.write(BATCHFILE % d) f.close() -- cgit v1.2.1 From e412a07c2f1b3e16c5951675529140a8a96ee08e Mon Sep 17 00:00:00 2001 From: Daniel Neuh?user Date: Sat, 8 May 2010 21:47:52 +0200 Subject: Encode even bytestrings containing ascii tests, they are unicode in python3 --- sphinx/util/__init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index 8d1298cd..c1e8d25c 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -168,11 +168,14 @@ def save_traceback(): """ exc = traceback.format_exc() fd, path = tempfile.mkstemp('.log', 'sphinx-err-') - os.write(fd, '# Sphinx version: %s\n' % sphinx.__version__) - os.write(fd, '# Docutils version: %s %s\n' % (docutils.__version__, - docutils.__version_details__)) - os.write(fd, '# Jinja2 version: %s\n' % jinja2.__version__) - os.write(fd, exc) + os.write(fd, + (('# Sphinx version: %s\n' + '# Docutils version: %s %s\n' + '# Jinja2 version: %s\n') % (sphinx.__version__, + docutils.__version__, + docutils.__version_details__, + jinja2.__version__)).encode('utf-8')) + os.write(fd, exc.encode('utf-8')) os.close(fd) return path -- cgit v1.2.1 From 6ee4f50b5b3a526519bf9a236e86cbd411eb61d0 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 18:08:00 +0200 Subject: Take string constant out of function. --- sphinx/util/__init__.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index c1e8d25c..2ef420ed 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -162,19 +162,22 @@ def copy_static_entry(source, targetdir, builder, context={}, shutil.copytree(source, target) +_DEBUG_HEADER = '''\ +# Sphinx version: %s +# Docutils version: %s %s +# Jinja2 version: %s +''' + def save_traceback(): """ Save the current exception's traceback in a temporary file. """ exc = traceback.format_exc() fd, path = tempfile.mkstemp('.log', 'sphinx-err-') - os.write(fd, - (('# Sphinx version: %s\n' - '# Docutils version: %s %s\n' - '# Jinja2 version: %s\n') % (sphinx.__version__, - docutils.__version__, - docutils.__version_details__, - jinja2.__version__)).encode('utf-8')) + os.write(fd, (_DEBUG_HEADER % + (sphinx.__version__, + docutils.__version__, docutils.__version_details__, + jinja2.__version__)).encode('utf-8')) os.write(fd, exc.encode('utf-8')) os.close(fd) return path -- cgit v1.2.1 From b2b313c98290c93e2d38d0abb9b04f373775baa1 Mon Sep 17 00:00:00 2001 From: Daniel Neuh?user Date: Sat, 8 May 2010 22:00:15 +0200 Subject: Use code objects for exec statements instead of files --- sphinx/config.py | 3 ++- tests/test_quickstart.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/sphinx/config.py b/sphinx/config.py index c22a5ee7..2ec76987 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -167,9 +167,10 @@ class Config(object): os.chdir(dirname) f = open(config_file, 'U') try: - exec f in config + code = compile(f.read(), config_file, 'exec') finally: f.close() + exec code in config except SyntaxError, err: raise ConfigError('There is a syntax error in your ' 'configuration file: ' + str(err)) diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index 71ca95a4..8acff588 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -87,9 +87,10 @@ def test_quickstart_defaults(tempdir): ns = {} f = open(conffile, 'U') try: - exec f in ns + code = compile(f.read(), conffile, 'exec') finally: f.close() + exec code in ns assert ns['extensions'] == [] assert ns['templates_path'] == ['_templates'] assert ns['source_suffix'] == '.rst' @@ -144,9 +145,10 @@ def test_quickstart_all_answers(tempdir): ns = {} f = open(conffile, 'U') try: - exec f in ns + code = compile(f.read(), conffile, 'exec') finally: f.close() + exec code in ns assert ns['extensions'] == ['sphinx.ext.autodoc', 'sphinx.ext.doctest'] assert ns['templates_path'] == ['.templates'] assert ns['source_suffix'] == '.txt' -- cgit v1.2.1 From a11042f68b89eabee09a6c83fcae032ef3b9a9ea Mon Sep 17 00:00:00 2001 From: DasIch Date: Sat, 8 May 2010 22:28:28 +0200 Subject: Removing unnecessary ez_setup.py --- ez_setup.py | 276 ------------------------------------------------------------ 1 file changed, 276 deletions(-) delete mode 100644 ez_setup.py diff --git a/ez_setup.py b/ez_setup.py deleted file mode 100644 index d24e845e..00000000 --- a/ez_setup.py +++ /dev/null @@ -1,276 +0,0 @@ -#!python -"""Bootstrap setuptools installation - -If you want to use setuptools in your package's setup.py, just include this -file in the same directory with it, and add this to the top of your setup.py:: - - from ez_setup import use_setuptools - use_setuptools() - -If you want to require a specific version of setuptools, set a download -mirror, or use an alternate download directory, you can do so by supplying -the appropriate options to ``use_setuptools()``. - -This file can also be run as a script to install or upgrade setuptools. -""" -import sys -DEFAULT_VERSION = "0.6c9" -DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] - -md5_data = { - 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', - 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', - 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', - 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', - 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', - 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', - 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', - 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', - 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', - 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', - 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', - 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', - 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', - 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', - 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', - 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', - 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', - 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', - 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', - 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', - 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', - 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', - 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', - 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', - 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', - 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', - 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', - 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', - 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', - 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', - 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', - 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', - 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', - 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', -} - -import sys, os -try: from hashlib import md5 -except ImportError: from md5 import md5 - -def _validate_md5(egg_name, data): - if egg_name in md5_data: - digest = md5(data).hexdigest() - if digest != md5_data[egg_name]: - print >>sys.stderr, ( - "md5 validation of %s failed! (Possible download problem?)" - % egg_name - ) - sys.exit(2) - return data - -def use_setuptools( - version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, - download_delay=15 -): - """Automatically find/download setuptools and make it available on sys.path - - `version` should be a valid setuptools version number that is available - as an egg for download under the `download_base` URL (which should end with - a '/'). `to_dir` is the directory where setuptools will be downloaded, if - it is not already available. If `download_delay` is specified, it should - be the number of seconds that will be paused before initiating a download, - should one be required. If an older version of setuptools is installed, - this routine will print a message to ``sys.stderr`` and raise SystemExit in - an attempt to abort the calling script. - """ - was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules - def do_download(): - egg = download_setuptools(version, download_base, to_dir, download_delay) - sys.path.insert(0, egg) - import setuptools; setuptools.bootstrap_install_from = egg - try: - import pkg_resources - except ImportError: - return do_download() - try: - pkg_resources.require("setuptools>="+version); return - except pkg_resources.VersionConflict, e: - if was_imported: - print >>sys.stderr, ( - "The required version of setuptools (>=%s) is not available, and\n" - "can't be installed while this script is running. Please install\n" - " a more recent version first, using 'easy_install -U setuptools'." - "\n\n(Currently using %r)" - ) % (version, e.args[0]) - sys.exit(2) - else: - del pkg_resources, sys.modules['pkg_resources'] # reload ok - return do_download() - except pkg_resources.DistributionNotFound: - return do_download() - -def download_setuptools( - version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, - delay = 15 -): - """Download setuptools from a specified location and return its filename - - `version` should be a valid setuptools version number that is available - as an egg for download under the `download_base` URL (which should end - with a '/'). `to_dir` is the directory where the egg will be downloaded. - `delay` is the number of seconds to pause before an actual download attempt. - """ - import urllib2, shutil - egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) - url = download_base + egg_name - saveto = os.path.join(to_dir, egg_name) - src = dst = None - if not os.path.exists(saveto): # Avoid repeated downloads - try: - from distutils import log - if delay: - log.warn(""" ---------------------------------------------------------------------------- -This script requires setuptools version %s to run (even to display -help). I will attempt to download it for you (from -%s), but -you may need to enable firewall access for this script first. -I will start the download in %d seconds. - -(Note: if this machine does not have network access, please obtain the file - - %s - -and place it in this directory before rerunning this script.) ----------------------------------------------------------------------------""", - version, download_base, delay, url - ); from time import sleep; sleep(delay) - log.warn("Downloading %s", url) - src = urllib2.urlopen(url) - # Read/write all in one block, so we don't create a corrupt file - # if the download is interrupted. - data = _validate_md5(egg_name, src.read()) - dst = open(saveto,"wb"); dst.write(data) - finally: - if src: src.close() - if dst: dst.close() - return os.path.realpath(saveto) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -def main(argv, version=DEFAULT_VERSION): - """Install or upgrade setuptools and EasyInstall""" - try: - import setuptools - except ImportError: - egg = None - try: - egg = download_setuptools(version, delay=0) - sys.path.insert(0,egg) - from setuptools.command.easy_install import main - return main(list(argv)+[egg]) # we're done here - finally: - if egg and os.path.exists(egg): - os.unlink(egg) - else: - if setuptools.__version__ == '0.0.1': - print >>sys.stderr, ( - "You have an obsolete version of setuptools installed. Please\n" - "remove it from your system entirely before rerunning this script." - ) - sys.exit(2) - - req = "setuptools>="+version - import pkg_resources - try: - pkg_resources.require(req) - except pkg_resources.VersionConflict: - try: - from setuptools.command.easy_install import main - except ImportError: - from easy_install import main - main(list(argv)+[download_setuptools(delay=0)]) - sys.exit(0) # try to force an exit - else: - if argv: - from setuptools.command.easy_install import main - main(argv) - else: - print "Setuptools version",version,"or greater has been installed." - print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' - -def update_md5(filenames): - """Update our built-in md5 registry""" - - import re - - for name in filenames: - base = os.path.basename(name) - f = open(name,'rb') - md5_data[base] = md5(f.read()).hexdigest() - f.close() - - data = [" %r: %r,\n" % it for it in md5_data.items()] - data.sort() - repl = "".join(data) - - import inspect - srcfile = inspect.getsourcefile(sys.modules[__name__]) - f = open(srcfile, 'rb'); src = f.read(); f.close() - - match = re.search("\nmd5_data = {\n([^}]+)}", src) - if not match: - print >>sys.stderr, "Internal error!" - sys.exit(2) - - src = src[:match.start(1)] + repl + src[match.end(1):] - f = open(srcfile,'w') - f.write(src) - f.close() - - -if __name__=='__main__': - if len(sys.argv)>2 and sys.argv[1]=='--md5update': - update_md5(sys.argv[2:]) - else: - main(sys.argv[1:]) - - - - - - -- cgit v1.2.1 From 66c05e5701e78e6990b1fe3a9ab6fb8145006768 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sat, 8 May 2010 22:33:36 +0200 Subject: Added a distribute_setup to replace ez_setup --- distribute_setup.py | 481 ++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 4 +- 2 files changed, 483 insertions(+), 2 deletions(-) create mode 100644 distribute_setup.py diff --git a/distribute_setup.py b/distribute_setup.py new file mode 100644 index 00000000..4f7bd08c --- /dev/null +++ b/distribute_setup.py @@ -0,0 +1,481 @@ +#!python +"""Bootstrap distribute installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from distribute_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import os +import sys +import time +import fnmatch +import tempfile +import tarfile +from distutils import log + +try: + from site import USER_SITE +except ImportError: + USER_SITE = None + +try: + import subprocess + + def _python_cmd(*args): + args = (sys.executable,) + args + return subprocess.call(args) == 0 + +except ImportError: + # will be used for python 2.3 + def _python_cmd(*args): + args = (sys.executable,) + args + # quoting arguments if windows + if sys.platform == 'win32': + def quote(arg): + if ' ' in arg: + return '"%s"' % arg + return arg + args = [quote(arg) for arg in args] + return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 + +DEFAULT_VERSION = "0.6.12" +DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" +SETUPTOOLS_FAKED_VERSION = "0.6c11" + +SETUPTOOLS_PKG_INFO = """\ +Metadata-Version: 1.0 +Name: setuptools +Version: %s +Summary: xxxx +Home-page: xxx +Author: xxx +Author-email: xxx +License: xxx +Description: xxx +""" % SETUPTOOLS_FAKED_VERSION + + +def _install(tarball): + # extracting the tarball + tmpdir = tempfile.mkdtemp() + log.warn('Extracting in %s', tmpdir) + old_wd = os.getcwd() + try: + os.chdir(tmpdir) + tar = tarfile.open(tarball) + _extractall(tar) + tar.close() + + # going in the directory + subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) + os.chdir(subdir) + log.warn('Now working in %s', subdir) + + # installing + log.warn('Installing Distribute') + if not _python_cmd('setup.py', 'install'): + log.warn('Something went wrong during the installation.') + log.warn('See the error message above.') + finally: + os.chdir(old_wd) + + +def _build_egg(egg, tarball, to_dir): + # extracting the tarball + tmpdir = tempfile.mkdtemp() + log.warn('Extracting in %s', tmpdir) + old_wd = os.getcwd() + try: + os.chdir(tmpdir) + tar = tarfile.open(tarball) + _extractall(tar) + tar.close() + + # going in the directory + subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) + os.chdir(subdir) + log.warn('Now working in %s', subdir) + + # building an egg + log.warn('Building a Distribute egg in %s', to_dir) + _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) + + finally: + os.chdir(old_wd) + # returning the result + log.warn(egg) + if not os.path.exists(egg): + raise IOError('Could not build the egg.') + + +def _do_download(version, download_base, to_dir, download_delay): + egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' + % (version, sys.version_info[0], sys.version_info[1])) + if not os.path.exists(egg): + tarball = download_setuptools(version, download_base, + to_dir, download_delay) + _build_egg(egg, tarball, to_dir) + sys.path.insert(0, egg) + import setuptools + setuptools.bootstrap_install_from = egg + + +def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, + to_dir=os.curdir, download_delay=15, no_fake=True): + # making sure we use the absolute path + to_dir = os.path.abspath(to_dir) + was_imported = 'pkg_resources' in sys.modules or \ + 'setuptools' in sys.modules + try: + try: + import pkg_resources + if not hasattr(pkg_resources, '_distribute'): + if not no_fake: + _fake_setuptools() + raise ImportError + except ImportError: + return _do_download(version, download_base, to_dir, download_delay) + try: + pkg_resources.require("distribute>="+version) + return + except pkg_resources.VersionConflict: + e = sys.exc_info()[1] + if was_imported: + sys.stderr.write( + "The required version of distribute (>=%s) is not available,\n" + "and can't be installed while this script is running. Please\n" + "install a more recent version first, using\n" + "'easy_install -U distribute'." + "\n\n(Currently using %r)\n" % (version, e.args[0])) + sys.exit(2) + else: + del pkg_resources, sys.modules['pkg_resources'] # reload ok + return _do_download(version, download_base, to_dir, + download_delay) + except pkg_resources.DistributionNotFound: + return _do_download(version, download_base, to_dir, + download_delay) + finally: + if not no_fake: + _create_fake_setuptools_pkg_info(to_dir) + +def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, + to_dir=os.curdir, delay=15): + """Download distribute from a specified location and return its filename + + `version` should be a valid distribute version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download + attempt. + """ + # making sure we use the absolute path + to_dir = os.path.abspath(to_dir) + try: + from urllib.request import urlopen + except ImportError: + from urllib2 import urlopen + tgz_name = "distribute-%s.tar.gz" % version + url = download_base + tgz_name + saveto = os.path.join(to_dir, tgz_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + log.warn("Downloading %s", url) + src = urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = src.read() + dst = open(saveto, "wb") + dst.write(data) + finally: + if src: + src.close() + if dst: + dst.close() + return os.path.realpath(saveto) + +def _no_sandbox(function): + def __no_sandbox(*args, **kw): + try: + from setuptools.sandbox import DirectorySandbox + if not hasattr(DirectorySandbox, '_old'): + def violation(*args): + pass + DirectorySandbox._old = DirectorySandbox._violation + DirectorySandbox._violation = violation + patched = True + else: + patched = False + except ImportError: + patched = False + + try: + return function(*args, **kw) + finally: + if patched: + DirectorySandbox._violation = DirectorySandbox._old + del DirectorySandbox._old + + return __no_sandbox + +@_no_sandbox +def _patch_file(path, content): + """Will backup the file then patch it""" + existing_content = open(path).read() + if existing_content == content: + # already patched + log.warn('Already patched.') + return False + log.warn('Patching...') + _rename_path(path) + f = open(path, 'w') + try: + f.write(content) + finally: + f.close() + return True + + +def _same_content(path, content): + return open(path).read() == content + +def _rename_path(path): + new_name = path + '.OLD.%s' % time.time() + log.warn('Renaming %s into %s', path, new_name) + os.rename(path, new_name) + return new_name + +@_no_sandbox +def _remove_flat_installation(placeholder): + if not os.path.isdir(placeholder): + log.warn('Unkown installation at %s', placeholder) + return False + found = False + for file in os.listdir(placeholder): + if fnmatch.fnmatch(file, 'setuptools*.egg-info'): + found = True + break + if not found: + log.warn('Could not locate setuptools*.egg-info') + return + + log.warn('Removing elements out of the way...') + pkg_info = os.path.join(placeholder, file) + if os.path.isdir(pkg_info): + patched = _patch_egg_dir(pkg_info) + else: + patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) + + if not patched: + log.warn('%s already patched.', pkg_info) + return False + # now let's move the files out of the way + for element in ('setuptools', 'pkg_resources.py', 'site.py'): + element = os.path.join(placeholder, element) + if os.path.exists(element): + _rename_path(element) + else: + log.warn('Could not find the %s element of the ' + 'Setuptools distribution', element) + return True + + +def _after_install(dist): + log.warn('After install bootstrap.') + placeholder = dist.get_command_obj('install').install_purelib + _create_fake_setuptools_pkg_info(placeholder) + +@_no_sandbox +def _create_fake_setuptools_pkg_info(placeholder): + if not placeholder or not os.path.exists(placeholder): + log.warn('Could not find the install location') + return + pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) + setuptools_file = 'setuptools-%s-py%s.egg-info' % \ + (SETUPTOOLS_FAKED_VERSION, pyver) + pkg_info = os.path.join(placeholder, setuptools_file) + if os.path.exists(pkg_info): + log.warn('%s already exists', pkg_info) + return + + log.warn('Creating %s', pkg_info) + f = open(pkg_info, 'w') + try: + f.write(SETUPTOOLS_PKG_INFO) + finally: + f.close() + + pth_file = os.path.join(placeholder, 'setuptools.pth') + log.warn('Creating %s', pth_file) + f = open(pth_file, 'w') + try: + f.write(os.path.join(os.curdir, setuptools_file)) + finally: + f.close() + +@_no_sandbox +def _patch_egg_dir(path): + # let's check if it's already patched + pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') + if os.path.exists(pkg_info): + if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): + log.warn('%s already patched.', pkg_info) + return False + _rename_path(path) + os.mkdir(path) + os.mkdir(os.path.join(path, 'EGG-INFO')) + pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') + f = open(pkg_info, 'w') + try: + f.write(SETUPTOOLS_PKG_INFO) + finally: + f.close() + return True + + +def _before_install(): + log.warn('Before install bootstrap.') + _fake_setuptools() + + +def _under_prefix(location): + if 'install' not in sys.argv: + return True + args = sys.argv[sys.argv.index('install')+1:] + for index, arg in enumerate(args): + for option in ('--root', '--prefix'): + if arg.startswith('%s=' % option): + top_dir = arg.split('root=')[-1] + return location.startswith(top_dir) + elif arg == option: + if len(args) > index: + top_dir = args[index+1] + return location.startswith(top_dir) + elif option == '--user' and USER_SITE is not None: + return location.startswith(USER_SITE) + return True + + +def _fake_setuptools(): + log.warn('Scanning installed packages') + try: + import pkg_resources + except ImportError: + # we're cool + log.warn('Setuptools or Distribute does not seem to be installed.') + return + ws = pkg_resources.working_set + try: + setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', + replacement=False)) + except TypeError: + # old distribute API + setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) + + if setuptools_dist is None: + log.warn('No setuptools distribution found') + return + # detecting if it was already faked + setuptools_location = setuptools_dist.location + log.warn('Setuptools installation detected at %s', setuptools_location) + + # if --root or --preix was provided, and if + # setuptools is not located in them, we don't patch it + if not _under_prefix(setuptools_location): + log.warn('Not patching, --root or --prefix is installing Distribute' + ' in another location') + return + + # let's see if its an egg + if not setuptools_location.endswith('.egg'): + log.warn('Non-egg installation') + res = _remove_flat_installation(setuptools_location) + if not res: + return + else: + log.warn('Egg installation') + pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') + if (os.path.exists(pkg_info) and + _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): + log.warn('Already patched.') + return + log.warn('Patching...') + # let's create a fake egg replacing setuptools one + res = _patch_egg_dir(setuptools_location) + if not res: + return + log.warn('Patched done.') + _relaunch() + + +def _relaunch(): + log.warn('Relaunching...') + # we have to relaunch the process + args = [sys.executable] + sys.argv + sys.exit(subprocess.call(args)) + + +def _extractall(self, path=".", members=None): + """Extract all members from the archive to the current working + directory and set owner, modification time and permissions on + directories afterwards. `path' specifies a different directory + to extract to. `members' is optional and must be a subset of the + list returned by getmembers(). + """ + import copy + import operator + from tarfile import ExtractError + directories = [] + + if members is None: + members = self + + for tarinfo in members: + if tarinfo.isdir(): + # Extract directories with a safe mode. + directories.append(tarinfo) + tarinfo = copy.copy(tarinfo) + tarinfo.mode = 448 # decimal for oct 0700 + self.extract(tarinfo, path) + + # Reverse sort directories. + if sys.version_info < (2, 4): + def sorter(dir1, dir2): + return cmp(dir1.name, dir2.name) + directories.sort(sorter) + directories.reverse() + else: + directories.sort(key=operator.attrgetter('name'), reverse=True) + + # Set correct owner, mtime and filemode on directories. + for tarinfo in directories: + dirpath = os.path.join(path, tarinfo.name) + try: + self.chown(tarinfo, dirpath) + self.utime(tarinfo, dirpath) + self.chmod(tarinfo, dirpath) + except ExtractError: + e = sys.exc_info()[1] + if self.errorlevel > 1: + raise + else: + self._dbg(1, "tarfile: %s" % e) + + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + tarball = download_setuptools() + _install(tarball) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/setup.py b/setup.py index 2494851f..8d06f569 100644 --- a/setup.py +++ b/setup.py @@ -2,8 +2,8 @@ try: from setuptools import setup, find_packages except ImportError: - import ez_setup - ez_setup.use_setuptools() + import distribute_setup + distribute_setup.use_setuptools() from setuptools import setup, find_packages import os -- cgit v1.2.1 From afa4efb7ab548205e24b74e0ada55ec5a27145c6 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 18:13:57 +0200 Subject: Add a constant for class types, which lacks types.ClassType in Py3k. --- sphinx/application.py | 3 --- sphinx/environment.py | 4 ++-- sphinx/ext/autodoc.py | 13 ++++--------- sphinx/util/nodes.py | 4 ++-- sphinx/util/pycompat.py | 12 ++++++++++++ 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/sphinx/application.py b/sphinx/application.py index 97778d3f..b3d2aebc 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -37,9 +37,6 @@ from sphinx.util.osutil import ENOENT from sphinx.util.console import bold -# Directive is either new-style or old-style -clstypes = (type, types.ClassType) - # List of all known core events. Maps name to arguments description. events = { 'builder-inited': '', diff --git a/sphinx/environment.py b/sphinx/environment.py index fa8460cb..21994a74 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -39,7 +39,7 @@ from sphinx.util import url_re, get_matching_docs, docname_join, \ from sphinx.util.nodes import clean_astext, make_refnode from sphinx.util.osutil import movefile, SEP, ustrftime from sphinx.util.matching import compile_matchers -from sphinx.util.pycompat import all +from sphinx.util.pycompat import all, class_types from sphinx.errors import SphinxError, ExtensionError from sphinx.locale import _ @@ -251,7 +251,7 @@ class BuildEnvironment: if key.startswith('_') or \ isinstance(val, types.ModuleType) or \ isinstance(val, types.FunctionType) or \ - isinstance(val, (type, types.ClassType)): + isinstance(val, class_types): del self.config[key] try: pickle.dump(self, picklefile, pickle.HIGHEST_PROTOCOL) diff --git a/sphinx/ext/autodoc.py b/sphinx/ext/autodoc.py index adf08bcd..8a827a91 100644 --- a/sphinx/ext/autodoc.py +++ b/sphinx/ext/autodoc.py @@ -14,7 +14,7 @@ import re import sys import inspect -from types import FunctionType, BuiltinFunctionType, MethodType, ClassType +from types import FunctionType, BuiltinFunctionType, MethodType from docutils import nodes from docutils.utils import assemble_option_dict @@ -27,15 +27,10 @@ from sphinx.application import ExtensionError from sphinx.util.nodes import nested_parse_with_titles from sphinx.util.compat import Directive from sphinx.util.inspect import isdescriptor, safe_getmembers, safe_getattr +from sphinx.util.pycompat import base_exception, class_types from sphinx.util.docstrings import prepare_docstring -try: - base_exception = BaseException -except NameError: - base_exception = Exception - - #: extended signature RE: with explicit module name separated by :: py_ext_sig_re = re.compile( r'''^ ([\w.]+::)? # explicit module name @@ -866,7 +861,7 @@ class ClassDocumenter(ModuleLevelDocumenter): @classmethod def can_document_member(cls, member, membername, isattr, parent): - return isinstance(member, (type, ClassType)) + return isinstance(member, class_types) def import_object(self): ret = ModuleLevelDocumenter.import_object(self) @@ -972,7 +967,7 @@ class ExceptionDocumenter(ClassDocumenter): @classmethod def can_document_member(cls, member, membername, isattr, parent): - return isinstance(member, (type, ClassType)) and \ + return isinstance(member, class_types) and \ issubclass(member, base_exception) diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 97b58569..aab8f014 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -10,11 +10,11 @@ """ import re -import types from docutils import nodes from sphinx import addnodes +from sphinx.util.pycompat import class_types # \x00 means the "<" was backslash-escaped @@ -115,7 +115,7 @@ def _new_traverse(self, condition=None, if include_self and descend and not siblings and not ascend: if condition is None: return self._all_traverse([]) - elif isinstance(condition, (types.ClassType, type)): + elif isinstance(condition, class_types): return self._fast_traverse(condition, []) return self._old_traverse(condition, include_self, descend, siblings, ascend) diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index bdd9507d..7bf768fa 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -13,6 +13,18 @@ import sys import codecs import encodings +try: + from types import ClassType + class_types = (type, ClassType) +except ImportError: + # Python 3 + class_types = (type,) + +try: + base_exception = BaseException +except NameError: + base_exception = Exception + try: any = any -- cgit v1.2.1 From 3ab89e534d271f8e5ee030fd260da7f16bfcc007 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sat, 8 May 2010 23:23:56 +0200 Subject: Use 'U' if file is not present (we run under 3.x) --- tests/path.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/path.py b/tests/path.py index 20deb048..7b89c0cd 100644 --- a/tests/path.py +++ b/tests/path.py @@ -64,8 +64,13 @@ except NameError: # Universal newline support _textmode = 'r' -if hasattr(file, 'newlines'): +try: + file +except NameError: _textmode = 'U' +else: + if hasattr(file, 'newlines'): + _textmode = 'U' class TreeWalkWarning(Warning): -- cgit v1.2.1 From 82661603b971d5be6cb292e8c169be9eeb136eba Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 18:18:57 +0200 Subject: Fix wrong qualified name. --- sphinx/quickstart.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sphinx/quickstart.py b/sphinx/quickstart.py index fe2b43a3..a63907c7 100644 --- a/sphinx/quickstart.py +++ b/sphinx/quickstart.py @@ -837,12 +837,12 @@ directly.''' if d['ext_intersphinx']: conf_text += INTERSPHINX_CONFIG - f = codecs.open(path.join(srcdir, 'conf.py'), 'w', encoding='utf-8') + f = open(path.join(srcdir, 'conf.py'), 'w', encoding='utf-8') f.write(conf_text) f.close() masterfile = path.join(srcdir, d['master'] + d['suffix']) - f = codecs.open(masterfile, 'w', encoding='utf-8') + f = open(masterfile, 'w', encoding='utf-8') f.write(MASTER_FILE % d) f.close() @@ -850,14 +850,14 @@ directly.''' d['rsrcdir'] = d['sep'] and 'source' or '.' d['rbuilddir'] = d['sep'] and 'build' or d['dot'] + 'build' # use binary mode, to avoid writing \r\n on Windows - f = codecs.open(path.join(d['path'], 'Makefile'), 'wb', encoding='utf-8') + f = open(path.join(d['path'], 'Makefile'), 'wb', encoding='utf-8') f.write(MAKEFILE % d) f.close() if d['batchfile']: d['rsrcdir'] = d['sep'] and 'source' or '.' d['rbuilddir'] = d['sep'] and 'build' or d['dot'] + 'build' - f = codecs.open(path.join(d['path'], 'make.bat'), 'w', encoding='utf-8') + f = open(path.join(d['path'], 'make.bat'), 'w', encoding='utf-8') f.write(BATCHFILE % d) f.close() -- cgit v1.2.1 From 1c968e303f920e0f6e39153c7a1f0e2048b2a64d Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 18:19:17 +0200 Subject: Make it easier for the test suite to override raw_input for test_quickstart. --- sphinx/quickstart.py | 5 ++++- tests/test_quickstart.py | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/sphinx/quickstart.py b/sphinx/quickstart.py index a63907c7..892bd641 100644 --- a/sphinx/quickstart.py +++ b/sphinx/quickstart.py @@ -21,6 +21,9 @@ from sphinx.util.console import purple, bold, red, turquoise, \ nocolor, color_terminal from sphinx.util import texescape +# function to get input from terminal -- overridden by the test suite +term_input = raw_input + PROMPT_PREFIX = '> ' @@ -657,7 +660,7 @@ def do_prompt(d, key, text, default=None, validator=nonempty): prompt = purple(PROMPT_PREFIX + '%s [%s]: ' % (text, default)) else: prompt = purple(PROMPT_PREFIX + text + ': ') - x = raw_input(prompt) + x = term_input(prompt) if default and not x: x = default if not isinstance(x, unicode): diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index 8acff588..d0403d3b 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -37,7 +37,7 @@ def mock_raw_input(answers, needanswer=False): return raw_input def teardown_module(): - qs.raw_input = __builtin__.raw_input + qs.term_input = raw_input qs.TERM_ENCODING = getattr(sys.stdin, 'encoding', None) coloron() @@ -51,7 +51,7 @@ def test_do_prompt(): 'Q5': 'no', 'Q6': 'foo', } - qs.raw_input = mock_raw_input(answers) + qs.term_input = mock_raw_input(answers) try: qs.do_prompt(d, 'k1', 'Q1') except AssertionError: @@ -79,7 +79,7 @@ def test_quickstart_defaults(tempdir): 'Author name': 'Georg Brandl', 'Project version': '0.1', } - qs.raw_input = mock_raw_input(answers) + qs.term_input = mock_raw_input(answers) qs.inner_main([]) conffile = tempdir / 'conf.py' @@ -136,7 +136,7 @@ def test_quickstart_all_answers(tempdir): 'Create Windows command file': 'no', 'Do you want to use the epub builder': 'yes', } - qs.raw_input = mock_raw_input(answers, needanswer=True) + qs.term_input = mock_raw_input(answers, needanswer=True) qs.TERM_ENCODING = 'utf-8' qs.inner_main([]) -- cgit v1.2.1 From dd8aac39ece4a01f64ad2f146751ddba2b07035e Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 9 May 2010 00:38:16 +0200 Subject: Changed tests/run.py so that it's possible to run the testsuite on python3 more easiely --- Makefile | 2 +- tests/run.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 682f0366..593c7ad4 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PYTHON ?= python -3 +PYTHON ?= python3 export PYTHONPATH = $(shell echo "$$PYTHONPATH"):./sphinx diff --git a/tests/run.py b/tests/run.py index 0cb41442..5fd30d62 100755 --- a/tests/run.py +++ b/tests/run.py @@ -11,7 +11,16 @@ """ import sys -from os import path +from os import path, chdir + +if sys.version_info >= (3,): + print('Copying and converting sources to build/lib/test...') + from distutils.util import copydir_run_2to3 + testroot = path.dirname(__file__) or '.' + newroot = path.join(testroot, path.pardir, 'build', 'lib', 'test') + copydir_run_2to3(testroot, newroot) + # switch to the converted dir so nose tests the right tests + chdir(newroot) # always test the sphinx package from this directory sys.path.insert(0, path.join(path.dirname(__file__), path.pardir)) @@ -19,8 +28,8 @@ sys.path.insert(0, path.join(path.dirname(__file__), path.pardir)) try: import nose except ImportError: - print "The nose package is needed to run the Sphinx test suite." + print("The nose package is needed to run the Sphinx test suite.") sys.exit(1) -print "Running Sphinx test suite..." +print("Running Sphinx test suite...") nose.main() -- cgit v1.2.1 From d56bf3e26d8707a4e5a9174609c203ec34417cd1 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 9 May 2010 00:54:14 +0200 Subject: Fixed DefExpr.__str__ --- sphinx/domains/cpp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 90c3533e..8df89459 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -110,7 +110,7 @@ class DefinitionError(Exception): return self.description def __str__(self): - return unicode(self.encode('utf-8')) + return unicode(self).encode('utf-8') class DefExpr(object): -- cgit v1.2.1 From 7e84c4a69e87a7e8b60c9b92c61f11381e8f96f6 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 9 May 2010 14:57:18 +0200 Subject: Rename __unicode__ to __str__ --- custom_fixers/__init__.py | 0 custom_fixers/fix_alt_unicode.py | 12 ++++++++++++ setup.py | 1 + 3 files changed, 13 insertions(+) create mode 100644 custom_fixers/__init__.py create mode 100644 custom_fixers/fix_alt_unicode.py diff --git a/custom_fixers/__init__.py b/custom_fixers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/custom_fixers/fix_alt_unicode.py b/custom_fixers/fix_alt_unicode.py new file mode 100644 index 00000000..55175e90 --- /dev/null +++ b/custom_fixers/fix_alt_unicode.py @@ -0,0 +1,12 @@ +from lib2to3.fixer_base import BaseFix +from lib2to3.fixer_util import Name + +class FixAltUnicode(BaseFix): + PATTERN = """ + func=funcdef< 'def' name='__unicode__' + parameters< '(' NAME ')' > any+ > + """ + + def transform(self, node, results): + name = results['name'] + name.replace(Name('__str__', prefix=name.prefix)) diff --git a/setup.py b/setup.py index 8d06f569..fe4066b8 100644 --- a/setup.py +++ b/setup.py @@ -199,4 +199,5 @@ setup( install_requires=requires, cmdclass=cmdclass, use_2to3=True, + use_2to3_fixers=['custom_fixers'], ) -- cgit v1.2.1 From 7f113a128405f9d5732c9ab0ce6921541c2a3143 Mon Sep 17 00:00:00 2001 From: DasIch Date: Mon, 10 May 2010 00:59:50 +0200 Subject: Added information about the i did during the weekend to the changes file --- CHANGES.DasIch | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGES.DasIch b/CHANGES.DasIch index 856a5d87..711f9a53 100644 --- a/CHANGES.DasIch +++ b/CHANGES.DasIch @@ -5,6 +5,17 @@ This file contains changes made by Daniel Neuhäuser, during the Google Summer of Code 2010, to port Sphinx to Python 3.x. Changes are ordered descending by date. +May 10: Fixed a couple of tests and made several small changes. + +May 9: - Removed ez_setup.py which does not work with Python 3.x. and replaced + it with distribute_setup.py + - Use distribute (at least on 3.x) in order to run 2to3 automatically. + - Reverted some of the changes made in revision bac40c7c924c which + caused errors. + - Modified tests/run.py to test against the build created by + setup.py build in order to run the test suite with 3.x + - Several small changes to fix 3.x compatibilty. + May 1: - Removed deprecated tuple parameter unpacking. - Removed a pre-2.3 workaround for booleans because this creates a deprecation warning for 3.x, in which you can't assign values to -- cgit v1.2.1 From 37d6f507052f2e2b5723a42c4011e7382f98199d Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 16 May 2010 14:00:50 +0200 Subject: Added a clean-backupfiles target to the makefile --- Makefile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 593c7ad4..aa279514 100644 --- a/Makefile +++ b/Makefile @@ -12,17 +12,20 @@ check: -i doc/_build -i ez_setup.py -i tests/path.py -i tests/coverage.py \ -i env -i .tox . -clean: clean-pyc clean-patchfiles +clean: clean-pyc clean-patchfiles clean-backupfiles clean-pyc: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + - find . -name '*~' -exec rm -f {} + clean-patchfiles: find . -name '*.orig' -exec rm -f {} + find . -name '*.rej' -exec rm -f {} + +clean-backupfiles: + find . -name '*~' -exec rm -f {} + + find . -name '*.bak' -exec rm -f {} + + pylint: @pylint --rcfile utils/pylintrc sphinx -- cgit v1.2.1 From 23612f8aef9ccdf1d69e6561dc2a8871ca9399c0 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 16 May 2010 16:20:44 +0200 Subject: Add setup_distribute.py to MANIFEST.in and remove ez_setup.py --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 25cbc334..5e3104a8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,7 +7,7 @@ include TODO include babel.cfg include Makefile -include ez_setup.py +include setup_distribute.py include sphinx-autogen.py include sphinx-build.py include sphinx-quickstart.py -- cgit v1.2.1 From 4648596f35777e509b3718396e3c8aa97829cc2b Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 16 May 2010 16:54:15 +0200 Subject: Added a build target to the Makefile which we need for python3 tests --- Makefile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index aa279514..72e04772 100644 --- a/Makefile +++ b/Makefile @@ -32,8 +32,11 @@ pylint: reindent: @$(PYTHON) utils/reindent.py -r -B . -test: +test: build @cd tests; $(PYTHON) run.py -d -m '^[tT]est' $(TEST) -covertest: +covertest: build @cd tests; $(PYTHON) run.py -d -m '^[tT]est' --with-coverage --cover-package=sphinx $(TEST) + +build: + @$(PYTHON) setup.py build -- cgit v1.2.1 From 048f02e974c8d37bde6cb437b069c645e9b1f9b6 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 16 May 2010 17:50:13 +0200 Subject: Switched check_sources.py from getopt to optparse --- utils/check_sources.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/utils/check_sources.py b/utils/check_sources.py index 0571ab1e..1b30f2dc 100755 --- a/utils/check_sources.py +++ b/utils/check_sources.py @@ -12,8 +12,8 @@ """ import sys, os, re -import getopt import cStringIO +from optparse import OptionParser from os.path import join, splitext, abspath @@ -165,34 +165,32 @@ def check_xhtml(fn, lines): def main(argv): - try: - gopts, args = getopt.getopt(argv[1:], "vi:") - except getopt.GetoptError: - print "Usage: %s [-v] [-i ignorepath]* [path]" % argv[0] - return 2 - opts = {} - for opt, val in gopts: - if opt == '-i': - val = abspath(val) - opts.setdefault(opt, []).append(val) + parser = OptionParser(usage='Usage: %prog [-v] [-i ignorepath]* [path]') + parser.add_option('-v', '--verbose', dest='verbose', default=False, + action='store_true') + parser.add_option('-i', '--ignore-path', dest='ignored_paths', + default=[], action='append') + options, args = parser.parse_args(argv[1:]) if len(args) == 0: path = '.' elif len(args) == 1: path = args[0] else: - print "Usage: %s [-v] [-i ignorepath]* [path]" % argv[0] - return 2 + print args + parser.error('No more then one path supported') - verbose = '-v' in opts + verbose = options.verbose + ignored_paths = set(abspath(p) for p in options.ignored_paths) num = 0 out = cStringIO.StringIO() for root, dirs, files in os.walk(path): - if '.svn' in dirs: - dirs.remove('.svn') - if '-i' in opts and abspath(root) in opts['-i']: + for vcs_dir in ['.svn', '.hg', '.git']: + if vcs_dir in dirs: + dirs.remove(vcs_dir) + if abspath(root) in ignored_paths: del dirs[:] continue in_check_pkg = root.startswith('./sphinx') @@ -201,7 +199,7 @@ def main(argv): fn = join(root, fn) if fn[:2] == './': fn = fn[2:] - if '-i' in opts and abspath(fn) in opts['-i']: + if abspath(fn) in ignored_paths: continue ext = splitext(fn)[1] -- cgit v1.2.1 From fb33cfbee735ecaea18879a8674eb96f19708228 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 16 May 2010 19:45:23 +0200 Subject: Fixed file opening --- utils/check_sources.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/utils/check_sources.py b/utils/check_sources.py index 1b30f2dc..8eeadbf4 100755 --- a/utils/check_sources.py +++ b/utils/check_sources.py @@ -212,7 +212,10 @@ def main(argv): try: f = open(fn, 'r') - lines = list(f) + try: + lines = list(f) + finally: + f.close() except (IOError, OSError), err: print "%s: cannot open: %s" % (fn, err) num += 1 -- cgit v1.2.1 From e8a7fff96dc0dbe25ef482fa9feffa47daacd8dc Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 16 May 2010 22:19:38 +0200 Subject: Scripts in utils are now automatically converted. They may not work at the moment though --- Makefile | 14 ++++++++++---- utils/convert.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 utils/convert.py diff --git a/Makefile b/Makefile index 72e04772..53424b5d 100644 --- a/Makefile +++ b/Makefile @@ -2,17 +2,17 @@ PYTHON ?= python3 export PYTHONPATH = $(shell echo "$$PYTHONPATH"):./sphinx -.PHONY: all check clean clean-pyc clean-patchfiles pylint reindent test +.PHONY: all check clean clean-pyc clean-patchfiles clean-generated pylint reindent test all: clean-pyc check test -check: +check: convert-utils @$(PYTHON) utils/check_sources.py -i build -i dist -i sphinx/style/jquery.js \ -i sphinx/pycode/pgen2 -i sphinx/util/smartypants.py -i .ropeproject \ -i doc/_build -i ez_setup.py -i tests/path.py -i tests/coverage.py \ -i env -i .tox . -clean: clean-pyc clean-patchfiles clean-backupfiles +clean: clean-pyc clean-patchfiles clean-backupfiles clean-generated clean-pyc: find . -name '*.pyc' -exec rm -f {} + @@ -26,10 +26,13 @@ clean-backupfiles: find . -name '*~' -exec rm -f {} + find . -name '*.bak' -exec rm -f {} + +clean-generated: + rm utils/*3.py* + pylint: @pylint --rcfile utils/pylintrc sphinx -reindent: +reindent: convert-utils @$(PYTHON) utils/reindent.py -r -B . test: build @@ -40,3 +43,6 @@ covertest: build build: @$(PYTHON) setup.py build + +convert-utils: + @python3 utils/convert.py -i utils/convert.py utils/ diff --git a/utils/convert.py b/utils/convert.py new file mode 100644 index 00000000..65b4d3bd --- /dev/null +++ b/utils/convert.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# coding: utf-8 +""" + Converts files with 2to3 + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Creates a Python 3 version of each file. + + The Python3 version of a file foo.py will be called foo3.py. + + :copyright: Copyright 2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" +import os +import sys +from glob import iglob +from optparse import OptionParser +from shutil import copy +from distutils.util import run_2to3 + +def main(argv): + parser = OptionParser(usage='%prog [path]') + parser.add_option('-i', '--ignorepath', dest='ignored_paths', + action='append', default=[]) + options, args = parser.parse_args(argv) + + ignored_paths = set(options.ignored_paths) + + path = os.path.abspath(args[0]) if args else os.getcwd() + convertables = [] + for filename in iglob(os.path.join(path, '*.py')): + if filename in ignored_paths: + continue + basename, ext = os.path.splitext(filename) + if basename.endswith('3'): + continue + filename3 = basename + '3' + ext + copy(filename, filename3) + convertables.append(filename3) + run_2to3(convertables) + +if __name__ == "__main__": + main(sys.argv[1:]) -- cgit v1.2.1 From b376f209943055f03bb451894cda26a13b864b75 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 18:28:34 +0200 Subject: Ignore failures in removing converted utils. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 53424b5d..4c0cdef8 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ clean-backupfiles: find . -name '*.bak' -exec rm -f {} + clean-generated: - rm utils/*3.py* + -rm utils/*3.py* pylint: @pylint --rcfile utils/pylintrc sphinx -- cgit v1.2.1 From 11765efc0e1f4f375c2389fd57456e8954d1bf42 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 16 May 2010 22:24:20 +0200 Subject: convert.py now properly ignores paths --- utils/convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/convert.py b/utils/convert.py index 65b4d3bd..f025c49a 100644 --- a/utils/convert.py +++ b/utils/convert.py @@ -24,7 +24,7 @@ def main(argv): action='append', default=[]) options, args = parser.parse_args(argv) - ignored_paths = set(options.ignored_paths) + ignored_paths = {os.path.abspath(p) for p in options.ignored_paths} path = os.path.abspath(args[0]) if args else os.getcwd() convertables = [] -- cgit v1.2.1 From fa83e1bb0a5f86c46c4242b09ed2937de215748e Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 16 May 2010 22:27:05 +0200 Subject: Ignore generated files --- .hgignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgignore b/.hgignore index 70ea36a5..40c00aac 100644 --- a/.hgignore +++ b/.hgignore @@ -15,3 +15,4 @@ ^env/ \.DS_Store$ ~$ +^utils/.*3\.py$ -- cgit v1.2.1 From c69f37339b1832f69a7820c50a8bcc1b21eb3ae8 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 16 May 2010 23:51:25 +0200 Subject: Automatically use converted scripts in the makefile --- Makefile | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 4c0cdef8..2672aad4 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,22 @@ -PYTHON ?= python3 +PYTHON ?= python export PYTHONPATH = $(shell echo "$$PYTHONPATH"):./sphinx .PHONY: all check clean clean-pyc clean-patchfiles clean-generated pylint reindent test +DONT_CHECK = -i build -i dist -i sphinx/style/jquery.js -i sphinx/pycode/pgen2 \ + -i sphinx/util/smartypants.py -i .ropeproject -i doc/_build -i tests/path.py \ + -i tests/coverage.py -i env -i utils/convert.py -i utils/reindent3.py \ + -i utils/check_sources3.py + all: clean-pyc check test check: convert-utils - @$(PYTHON) utils/check_sources.py -i build -i dist -i sphinx/style/jquery.js \ - -i sphinx/pycode/pgen2 -i sphinx/util/smartypants.py -i .ropeproject \ - -i doc/_build -i ez_setup.py -i tests/path.py -i tests/coverage.py \ - -i env -i .tox . +ifeq ($(PYTHON), python3) + @$(PYTHON) utils/check_sources3.py $(DONT_CHECK) . +else + @$(PYTHON) utils/check_sources.py $(DONT_CHECK) . +endif clean: clean-pyc clean-patchfiles clean-backupfiles clean-generated @@ -33,7 +39,11 @@ pylint: @pylint --rcfile utils/pylintrc sphinx reindent: convert-utils +ifeq ($(PYTHON), python3) + @$(PYTHON) utils/reindent3.py -r -B . +else @$(PYTHON) utils/reindent.py -r -B . +endif test: build @cd tests; $(PYTHON) run.py -d -m '^[tT]est' $(TEST) -- cgit v1.2.1 From 9cfb1bb68cdd54761c67aff19965307dec270e53 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 16 May 2010 23:56:44 +0200 Subject: Keep under 80 chars per line in the Makefile --- Makefile | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 2672aad4..cf4f292f 100644 --- a/Makefile +++ b/Makefile @@ -2,12 +2,14 @@ PYTHON ?= python export PYTHONPATH = $(shell echo "$$PYTHONPATH"):./sphinx -.PHONY: all check clean clean-pyc clean-patchfiles clean-generated pylint reindent test +.PHONY: all check clean clean-pyc clean-patchfiles clean-generated pylint \ + reindent test -DONT_CHECK = -i build -i dist -i sphinx/style/jquery.js -i sphinx/pycode/pgen2 \ - -i sphinx/util/smartypants.py -i .ropeproject -i doc/_build -i tests/path.py \ - -i tests/coverage.py -i env -i utils/convert.py -i utils/reindent3.py \ - -i utils/check_sources3.py +DONT_CHECK = -i build -i dist -i sphinx/style/jquery.js \ + -i sphinx/pycode/pgen2 -i sphinx/util/smartypants.py \ + -i .ropeproject -i doc/_build -i tests/path.py \ + -i tests/coverage.py -i env -i utils/convert.py \ + -i utils/reindent3.py -i utils/check_sources3.py -i .tox all: clean-pyc check test @@ -49,7 +51,8 @@ test: build @cd tests; $(PYTHON) run.py -d -m '^[tT]est' $(TEST) covertest: build - @cd tests; $(PYTHON) run.py -d -m '^[tT]est' --with-coverage --cover-package=sphinx $(TEST) + @cd tests; $(PYTHON) run.py -d -m '^[tT]est' --with-coverage \ + --cover-package=sphinx $(TEST) build: @$(PYTHON) setup.py build -- cgit v1.2.1 From a9e0d6582b7604b5da9e1d438fd72844ce0a3218 Mon Sep 17 00:00:00 2001 From: DasIch Date: Mon, 17 May 2010 00:44:44 +0200 Subject: check_sources.py is now ported to 3.x --- Makefile | 2 +- utils/check_sources.py | 76 +++++++++++++++++++++++++++----------------------- 2 files changed, 42 insertions(+), 36 deletions(-) diff --git a/Makefile b/Makefile index cf4f292f..20930230 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PYTHON ?= python +PYTHON ?= python3 export PYTHONPATH = $(shell echo "$$PYTHONPATH"):./sphinx diff --git a/utils/check_sources.py b/utils/check_sources.py index 8eeadbf4..c412742b 100755 --- a/utils/check_sources.py +++ b/utils/check_sources.py @@ -16,6 +16,12 @@ import cStringIO from optparse import OptionParser from os.path import join, splitext, abspath +if sys.version_info >= (3, 0): + def b(s): + return s.encode('utf-8') +else: + b = str + checkers = {} @@ -30,26 +36,26 @@ def checker(*suffixes, **kwds): name_mail_re = r'[\w ]+(<.*?>)?' -copyright_re = re.compile(r'^ :copyright: Copyright 200\d(-20\d\d)? ' - r'by %s(, %s)*[,.]$' % - (name_mail_re, name_mail_re)) -license_re = re.compile(r" :license: (.*?).\n") -copyright_2_re = re.compile(r'^ %s(, %s)*[,.]$' % - (name_mail_re, name_mail_re)) -coding_re = re.compile(r'coding[:=]\s*([-\w.]+)') -not_ix_re = re.compile(r'\bnot\s+\S+?\s+i[sn]\s\S+') -is_const_re = re.compile(r'if.*?==\s+(None|False|True)\b') - -misspellings = ["developement", "adress", "verificate", # ALLOW-MISSPELLING - "informations"] # ALLOW-MISSPELLING - - -@checker('.py') -def check_syntax(fn, lines): - try: - compile(''.join(lines), fn, "exec") - except SyntaxError, err: - yield 0, "not compilable: %s" % err +copyright_re = re.compile(b(r'^ :copyright: Copyright 200\d(-20\d\d)? ' + r'by %s(, %s)*[,.]$' % + (name_mail_re, name_mail_re))) +license_re = re.compile(b(r" :license: (.*?).\n")) +copyright_2_re = re.compile(b(r'^ %s(, %s)*[,.]$' % + (name_mail_re, name_mail_re))) +coding_re = re.compile(b(r'coding[:=]\s*([-\w.]+)')) +not_ix_re = re.compile(b(r'\bnot\s+\S+?\s+i[sn]\s\S+')) +is_const_re = re.compile(b(r'if.*?==\s+(None|False|True)\b')) + +misspellings = [b("developement"), b("adress"), # ALLOW-MISSPELLING + b("verificate"), b("informations")] # ALLOW-MISSPELLING + +if sys.version_info < (3, 0): + @checker('.py') + def check_syntax(fn, lines): + try: + compile(b('').join(lines), fn, "exec") + except SyntaxError, err: + yield 0, "not compilable: %s" % err @checker('.py') @@ -61,8 +67,8 @@ def check_style_and_encoding(fn, lines): if lno < 2: co = coding_re.search(line) if co: - encoding = co.group(1) - if line.strip().startswith('#'): + encoding = co.group(1).decode('ascii') + if line.strip().startswith(b('#')): continue #m = not_ix_re.search(line) #if m: @@ -82,7 +88,7 @@ def check_style_and_encoding(fn, lines): def check_fileheader(fn, lines): # line number correction c = 1 - if lines[0:1] == ['#!/usr/bin/env python\n']: + if lines[0:1] == [b('#!/usr/bin/env python\n')]: lines = lines[1:] c = 2 @@ -91,38 +97,38 @@ def check_fileheader(fn, lines): for lno, l in enumerate(lines): llist.append(l) if lno == 0: - if l == '# -*- coding: rot13 -*-\n': + if l == b('# -*- coding: rot13 -*-\n'): # special-case pony package return - elif l != '# -*- coding: utf-8 -*-\n': + elif l != b('# -*- coding: utf-8 -*-\n'): yield 1, "missing coding declaration" elif lno == 1: - if l != '"""\n' and l != 'r"""\n': + if l != b('"""\n') and l != b('r"""\n'): yield 2, 'missing docstring begin (""")' else: docopen = True elif docopen: - if l == '"""\n': + if l == b('"""\n'): # end of docstring if lno <= 4: yield lno+c, "missing module name in docstring" break - if l != "\n" and l[:4] != ' ' and docopen: + if l != b("\n") and l[:4] != b(' ') and docopen: yield lno+c, "missing correct docstring indentation" if lno == 2: # if not in package, don't check the module name modname = fn[:-3].replace('/', '.').replace('.__init__', '') while modname: - if l.lower()[4:-1] == modname: + if l.lower()[4:-1] == b(modname): break modname = '.'.join(modname.split('.')[1:]) else: yield 3, "wrong module name in docstring heading" modnamelen = len(l.strip()) elif lno == 3: - if l.strip() != modnamelen * "~": + if l.strip() != modnamelen * b("~"): yield 4, "wrong module name underline, should be ~~~...~" else: @@ -145,16 +151,16 @@ def check_fileheader(fn, lines): @checker('.py', '.html', '.rst') def check_whitespace_and_spelling(fn, lines): for lno, line in enumerate(lines): - if "\t" in line: + if b("\t") in line: yield lno+1, "OMG TABS!!!1 " - if line[:-1].rstrip(' \t') != line[:-1]: + if line[:-1].rstrip(b(' \t')) != line[:-1]: yield lno+1, "trailing whitespace" for word in misspellings: - if word in line and 'ALLOW-MISSPELLING' not in line: + if word in line and b('ALLOW-MISSPELLING') not in line: yield lno+1, '"%s" used' % word -bad_tags = ('', '', '', '
', '', '', '', '
', ' Date: Mon, 17 May 2010 01:11:22 +0200 Subject: ported utils/reindent.py to python 3.x --- utils/reindent.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/utils/reindent.py b/utils/reindent.py index bcb6b434..63ff7ef7 100755 --- a/utils/reindent.py +++ b/utils/reindent.py @@ -47,6 +47,17 @@ recurse = 0 dryrun = 0 no_backup = 0 +if sys.version_info >= (3, 0): + def tokens(readline, tokeneater): + for token in tokenize.tokenize(readline): + tokeneater(*token) + + def b(s): + return s.encode('utf-8') +else: + tokens = tokenize.tokenize + b = str + def usage(msg=None): if msg is not None: print >> sys.stderr, msg @@ -106,7 +117,7 @@ def check(file): if verbose: print "checking", file, "...", try: - f = open(file) + f = open(file, 'rb') except IOError, msg: errprint("%s: I/O Error: %s" % (file, str(msg))) return @@ -129,7 +140,7 @@ def check(file): os.rename(file, bak) if verbose: print "renamed", file, "to", bak - f = open(file, "w") + f = open(file, "wb") r.write(f) f.close() if verbose: @@ -151,7 +162,7 @@ class Reindenter: # File lines, rstripped & tab-expanded. Dummy at start is so # that we can use tokenize's 1-based line numbering easily. # Note that a line is all-blank iff it's "\n". - self.lines = [line.rstrip('\n \t').expandtabs() + "\n" + self.lines = [line.rstrip(b('\n \t')).expandtabs() + b("\n") for line in self.raw] self.lines.insert(0, None) self.index = 1 # index into self.lines of next line @@ -163,10 +174,10 @@ class Reindenter: self.stats = [] def run(self): - tokenize.tokenize(self.getline, self.tokeneater) + tokens(self.getline, self.tokeneater) # Remove trailing empty lines. lines = self.lines - while lines and lines[-1] == "\n": + while lines and lines[-1] == b("\n"): lines.pop() # Sentinel. stats = self.stats @@ -222,10 +233,10 @@ class Reindenter: else: for line in lines[thisstmt:nextstmt]: if diff > 0: - if line == "\n": + if line == b("\n"): after.append(line) else: - after.append(" " * diff + line) + after.append(b(" ") * diff + line) else: remove = min(getlspace(line), -diff) after.append(line[remove:]) @@ -237,7 +248,7 @@ class Reindenter: # Line-getter for tokenize. def getline(self): if self.index >= len(self.lines): - line = "" + line = b("") else: line = self.lines[self.index] self.index += 1 @@ -286,7 +297,7 @@ class Reindenter: # Count number of leading blanks. def getlspace(line): i, n = 0, len(line) - while i < n and line[i] == " ": + while i < n and line[i] == b(" "): i += 1 return i -- cgit v1.2.1 From 3059def84f00c6c5e82510bbe6cca4bca56c6b01 Mon Sep 17 00:00:00 2001 From: DasIch Date: Mon, 17 May 2010 01:28:50 +0200 Subject: Ignore errors when removing generated files --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 20930230..c1b266dc 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ clean-backupfiles: find . -name '*.bak' -exec rm -f {} + clean-generated: - -rm utils/*3.py* + rm -f utils/*3.py* pylint: @pylint --rcfile utils/pylintrc sphinx -- cgit v1.2.1 From 0c90e4d3f88a2af2572f8b37cb534668f0eeec3d Mon Sep 17 00:00:00 2001 From: DasIch Date: Mon, 17 May 2010 02:23:59 +0200 Subject: Added latest changes to the Changes.DasIch file --- CHANGES.DasIch | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.DasIch b/CHANGES.DasIch index 711f9a53..3f716726 100644 --- a/CHANGES.DasIch +++ b/CHANGES.DasIch @@ -5,6 +5,12 @@ This file contains changes made by Daniel Neuhäuser, during the Google Summer of Code 2010, to port Sphinx to Python 3.x. Changes are ordered descending by date. +May 16: - Added utils/convert.py which converts entire directories of python + files with 2to3 and names the converted files foo3.py. + - Modified the Makefile so that in case Python 3 is used the scripts in + utils get converted with utils/convert.py and are used instead of the + Python 2 scripts. + May 10: Fixed a couple of tests and made several small changes. May 9: - Removed ez_setup.py which does not work with Python 3.x. and replaced -- cgit v1.2.1 From 3920289eaa99fde45b8656a25a102ab82da4e77d Mon Sep 17 00:00:00 2001 From: DasIch Date: Mon, 17 May 2010 02:26:31 +0200 Subject: Added latest reindent.py file --- Makefile | 4 +-- utils/reindent.py | 96 +++++++++++++++++++++++++++++-------------------------- 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/Makefile b/Makefile index c1b266dc..ecc7722c 100644 --- a/Makefile +++ b/Makefile @@ -42,9 +42,9 @@ pylint: reindent: convert-utils ifeq ($(PYTHON), python3) - @$(PYTHON) utils/reindent3.py -r -B . + @$(PYTHON) utils/reindent3.py -r -n . else - @$(PYTHON) utils/reindent.py -r -B . + @$(PYTHON) utils/reindent.py -r -n . endif test: build diff --git a/utils/reindent.py b/utils/reindent.py index 63ff7ef7..59828fd8 100755 --- a/utils/reindent.py +++ b/utils/reindent.py @@ -1,16 +1,14 @@ #! /usr/bin/env python # Released to the public domain, by Tim Peters, 03 October 2000. -# -B option added by Georg Brandl, 2006. """reindent [-d][-r][-v] [ path ... ] --d (--dryrun) Dry run. Analyze, but don't make any changes to files. --r (--recurse) Recurse. Search for all .py files in subdirectories too. --B (--no-backup) Don't write .bak backup files. --v (--verbose) Verbose. Print informative msgs; else only names of \ -changed files. --h (--help) Help. Print this usage information and exit. +-d (--dryrun) Dry run. Analyze, but don't make any changes to, files. +-r (--recurse) Recurse. Search for all .py files in subdirectories too. +-n (--nobackup) No backup. Does not make a ".bak" file before reindenting. +-v (--verbose) Verbose. Print informative msgs; else no output. +-h (--help) Help. Print this usage information and exit. Change Python (.py) files to use 4-space indents and no hard tab characters. Also trim excess spaces and tabs from ends of lines, and remove empty lines @@ -34,29 +32,30 @@ resulting .py file won't change it again). The hard part of reindenting is figuring out what to do with comment lines. So long as the input files get a clean bill of health from tabnanny.py, reindent should do a good job. + +The backup file is a copy of the one that is being reindented. The ".bak" +file is generated with shutil.copy(), but some corner cases regarding +user/group and permissions could leave the backup file more readable that +you'd prefer. You can always use the --nobackup option to prevent this. """ __version__ = "1" import tokenize -import os +import os, shutil import sys -verbose = 0 -recurse = 0 -dryrun = 0 -no_backup = 0 - if sys.version_info >= (3, 0): def tokens(readline, tokeneater): for token in tokenize.tokenize(readline): - tokeneater(*token) - - def b(s): - return s.encode('utf-8') + yield tokeneater(*token) else: tokens = tokenize.tokenize - b = str + +verbose = 0 +recurse = 0 +dryrun = 0 +makebackup = True def usage(msg=None): if msg is not None: @@ -72,12 +71,10 @@ def errprint(*args): def main(): import getopt - global verbose, recurse, dryrun, no_backup - + global verbose, recurse, dryrun, makebackup try: - opts, args = getopt.getopt(sys.argv[1:], "drvhB", - ["dryrun", "recurse", "verbose", "help", - "no-backup"]) + opts, args = getopt.getopt(sys.argv[1:], "drnvh", + ["dryrun", "recurse", "nobackup", "verbose", "help"]) except getopt.error, msg: usage(msg) return @@ -86,10 +83,10 @@ def main(): dryrun += 1 elif o in ('-r', '--recurse'): recurse += 1 + elif o in ('-n', '--nobackup'): + makebackup = False elif o in ('-v', '--verbose'): verbose += 1 - elif o in ('-B', '--no-backup'): - no_backup += 1 elif o in ('-h', '--help'): usage() return @@ -109,7 +106,8 @@ def check(file): for name in names: fullname = os.path.join(file, name) if ((recurse and os.path.isdir(fullname) and - not os.path.islink(fullname)) + not os.path.islink(fullname) and + not os.path.split(fullname)[1].startswith(".")) or name.lower().endswith(".py")): check(fullname) return @@ -117,7 +115,7 @@ def check(file): if verbose: print "checking", file, "...", try: - f = open(file, 'rb') + f = open(file) except IOError, msg: errprint("%s: I/O Error: %s" % (file, str(msg))) return @@ -129,26 +127,35 @@ def check(file): print "changed." if dryrun: print "But this is a dry run, so leaving it alone." - else: - print "reindented", file, \ - (dryrun and "(dry run => not really)" or "") if not dryrun: - if not no_backup: - bak = file + ".bak" - if os.path.exists(bak): - os.remove(bak) - os.rename(file, bak) + bak = file + ".bak" + if makebackup: + shutil.copyfile(file, bak) if verbose: - print "renamed", file, "to", bak - f = open(file, "wb") + print "backed up", file, "to", bak + f = open(file, "w") r.write(f) f.close() if verbose: print "wrote new", file + return True else: if verbose: print "unchanged." + return False + +def _rstrip(line, JUNK='\n \t'): + """Return line stripped of trailing spaces, tabs, newlines. + + Note that line.rstrip() instead also strips sundry control characters, + but at least one known Emacs user expects to keep junk like that, not + mentioning Barry by name or anything . + """ + i = len(line) + while i > 0 and line[i-1] in JUNK: + i -= 1 + return line[:i] class Reindenter: @@ -162,7 +169,7 @@ class Reindenter: # File lines, rstripped & tab-expanded. Dummy at start is so # that we can use tokenize's 1-based line numbering easily. # Note that a line is all-blank iff it's "\n". - self.lines = [line.rstrip(b('\n \t')).expandtabs() + b("\n") + self.lines = [_rstrip(line).expandtabs() + "\n" for line in self.raw] self.lines.insert(0, None) self.index = 1 # index into self.lines of next line @@ -177,7 +184,7 @@ class Reindenter: tokens(self.getline, self.tokeneater) # Remove trailing empty lines. lines = self.lines - while lines and lines[-1] == b("\n"): + while lines and lines[-1] == "\n": lines.pop() # Sentinel. stats = self.stats @@ -233,10 +240,10 @@ class Reindenter: else: for line in lines[thisstmt:nextstmt]: if diff > 0: - if line == b("\n"): + if line == "\n": after.append(line) else: - after.append(b(" ") * diff + line) + after.append(" " * diff + line) else: remove = min(getlspace(line), -diff) after.append(line[remove:]) @@ -248,20 +255,19 @@ class Reindenter: # Line-getter for tokenize. def getline(self): if self.index >= len(self.lines): - line = b("") + line = "" else: line = self.lines[self.index] self.index += 1 return line # Line-eater for tokenize. - def tokeneater(self, type, token, scell, end, line, + def tokeneater(self, type, token, (sline, scol), end, line, INDENT=tokenize.INDENT, DEDENT=tokenize.DEDENT, NEWLINE=tokenize.NEWLINE, COMMENT=tokenize.COMMENT, NL=tokenize.NL): - sline, scol = scell if type == NEWLINE: # A program statement, or ENDMARKER, will eventually follow, @@ -297,7 +303,7 @@ class Reindenter: # Count number of leading blanks. def getlspace(line): i, n = 0, len(line) - while i < n and line[i] == b(" "): + while i < n and line[i] == " ": i += 1 return i -- cgit v1.2.1 From 305c90ec8f4e09b4fff825bdef5bd3af78850e88 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 23 May 2010 01:34:52 +0200 Subject: make now works without python3 --- Makefile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index ecc7722c..21a87e36 100644 --- a/Makefile +++ b/Makefile @@ -11,12 +11,13 @@ DONT_CHECK = -i build -i dist -i sphinx/style/jquery.js \ -i tests/coverage.py -i env -i utils/convert.py \ -i utils/reindent3.py -i utils/check_sources3.py -i .tox -all: clean-pyc check test +all: clean-pyc clean-backupfiles check test -check: convert-utils ifeq ($(PYTHON), python3) +check: convert-utils @$(PYTHON) utils/check_sources3.py $(DONT_CHECK) . else +check: @$(PYTHON) utils/check_sources.py $(DONT_CHECK) . endif @@ -40,10 +41,11 @@ clean-generated: pylint: @pylint --rcfile utils/pylintrc sphinx -reindent: convert-utils ifeq ($(PYTHON), python3) +reindent: convert-utils @$(PYTHON) utils/reindent3.py -r -n . else +reindent: @$(PYTHON) utils/reindent.py -r -n . endif @@ -57,5 +59,7 @@ covertest: build build: @$(PYTHON) setup.py build +ifeq ($(PYTHON), python3) convert-utils: @python3 utils/convert.py -i utils/convert.py utils/ +endif -- cgit v1.2.1 From fc3b3cdf3cf62cc5d5d7bef3d6d965753edad6cd Mon Sep 17 00:00:00 2001 From: DasIch Date: Mon, 24 May 2010 17:32:00 +0200 Subject: test_autodoc.test_get_doc now passes --- sphinx/ext/autodoc.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/sphinx/ext/autodoc.py b/sphinx/ext/autodoc.py index 8a827a91..1113f97a 100644 --- a/sphinx/ext/autodoc.py +++ b/sphinx/ext/autodoc.py @@ -411,9 +411,11 @@ class Documenter(object): def get_doc(self, encoding=None): """Decode and return lines of the docstring(s) for the object.""" docstring = self.get_attr(self.object, '__doc__', None) - if docstring: - # make sure we have Unicode docstrings, then sanitize and split - # into lines + # make sure we have Unicode docstrings, then sanitize and split + # into lines + if isinstance(docstring, unicode): + return [prepare_docstring(docstring)] + elif docstring: return [prepare_docstring(force_decode(docstring, encoding))] return [] @@ -934,9 +936,12 @@ class ClassDocumenter(ModuleLevelDocumenter): docstrings = [initdocstring] else: docstrings.append(initdocstring) - - return [prepare_docstring(force_decode(docstring, encoding)) - for docstring in docstrings] + doc = [] + for docstring in docstrings: + if not isinstance(docstring, unicode): + docstring = force_decode(docstring, encoding) + doc.append(prepare_docstring(docstring)) + return doc def add_content(self, more_content, no_docstring=False): if self.doc_as_attr: -- cgit v1.2.1 From 2928e66c78713363f0bc24c2399226337a95d9d7 Mon Sep 17 00:00:00 2001 From: DasIch Date: Mon, 24 May 2010 17:35:43 +0200 Subject: use open instead of file --- tests/path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/path.py b/tests/path.py index 7b89c0cd..f27e58a9 100644 --- a/tests/path.py +++ b/tests/path.py @@ -516,7 +516,7 @@ class path(_base): def open(self, mode='r'): """ Open this file. Return a file object. """ - return file(self, mode) + return open(self, mode) def bytes(self): """ Open this file, read all bytes, return them as a string. """ -- cgit v1.2.1 From b2982ffa71bd2b91ac6320d9ed3ddcb58639cb83 Mon Sep 17 00:00:00 2001 From: DasIch Date: Mon, 24 May 2010 18:13:56 +0200 Subject: Workaround for 2to3 --- sphinx/ext/intersphinx.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 0a210879..fb1f0e4f 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -36,8 +36,10 @@ from sphinx.builders.html import INVENTORY_FILENAME handlers = [urllib2.ProxyHandler(), urllib2.HTTPRedirectHandler(), urllib2.HTTPHandler()] -if hasattr(urllib2, 'HTTPSHandler'): +try: handlers.append(urllib2.HTTPSHandler) +except NameError: + pass urllib2.install_opener(urllib2.build_opener(*handlers)) -- cgit v1.2.1 From 3f3b30f45d6b4c31cfe5695f6874ea2a856e010d Mon Sep 17 00:00:00 2001 From: DasIch Date: Mon, 24 May 2010 18:25:20 +0200 Subject: don't assume strings to be byte strings --- sphinx/pycode/__init__.py | 3 ++- sphinx/pycode/pgen2/literals.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sphinx/pycode/__init__.py b/sphinx/pycode/__init__.py index b8e2fded..cb9c0887 100644 --- a/sphinx/pycode/__init__.py +++ b/sphinx/pycode/__init__.py @@ -98,7 +98,8 @@ class AttrDocVisitor(nodes.NodeVisitor): if not pnode or pnode.type not in (token.INDENT, token.DEDENT): break prefix = pnode.get_prefix() - prefix = prefix.decode(self.encoding) + if not isinstance(prefix, unicode): + prefix = prefix.decode(self.encoding) docstring = prepare_commentdoc(prefix) self.add_docstring(node, docstring) diff --git a/sphinx/pycode/pgen2/literals.py b/sphinx/pycode/pgen2/literals.py index 31900291..d4893702 100644 --- a/sphinx/pycode/pgen2/literals.py +++ b/sphinx/pycode/pgen2/literals.py @@ -66,7 +66,7 @@ uni_escape_re = re.compile(r"\\(\'|\"|\\|[abfnrtv]|x.{0,2}|[0-7]{1,3}|" def evalString(s, encoding=None): regex = escape_re repl = escape - if encoding: + if encoding and not isinstance(s, unicode): s = s.decode(encoding) if s.startswith('u') or s.startswith('U'): regex = uni_escape_re -- cgit v1.2.1 From 6a24d9f7f17e469fd9f222613ef0353cb54f04b6 Mon Sep 17 00:00:00 2001 From: DasIch Date: Mon, 24 May 2010 19:41:02 +0200 Subject: fixed test_markup test --- sphinx/util/osutil.py | 11 +++++++---- tests/test_markup.py | 9 ++++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py index beab38cb..1010fb2f 100644 --- a/sphinx/util/osutil.py +++ b/sphinx/util/osutil.py @@ -14,6 +14,7 @@ import re import time import errno import shutil +import sys from os import path # Errnos that we need. @@ -124,7 +125,9 @@ no_fn_re = re.compile(r'[^a-zA-Z0-9_-]') def make_filename(string): return no_fn_re.sub('', string) - -def ustrftime(format, *args): - # strftime for unicode strings - return time.strftime(unicode(format).encode('utf-8'), *args).decode('utf-8') +if sys.version_info < (3, 0): + def ustrftime(format, *args): + # strftime for unicode strings + return time.strftime(unicode(format).encode('utf-8'), *args).decode('utf-8') +else: + ustrftime = time.strftime diff --git a/tests/test_markup.py b/tests/test_markup.py index 31817df6..6d6badbb 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -10,6 +10,7 @@ """ import re +import sys from util import * @@ -20,6 +21,12 @@ from sphinx.util import texescape from sphinx.writers.html import HTMLWriter, SmartyPantsHTMLTranslator from sphinx.writers.latex import LaTeXWriter, LaTeXTranslator +if sys.version_info > (3, 0): + def b(s): + return s.encode('utf-8') +else: + b = str + def setup_module(): global app, settings, parser texescape.init() # otherwise done by the latex builder @@ -50,7 +57,7 @@ class ForgivingLaTeXTranslator(LaTeXTranslator, ForgivingTranslator): def verify_re(rst, html_expected, latex_expected): - document = utils.new_document('test data', settings) + document = utils.new_document(b('test data'), settings) document['file'] = 'dummy' parser.parse(rst, document) for msg in document.traverse(nodes.system_message): -- cgit v1.2.1 From 695f93d286f8ea23fff15c92b8d675b36aa592db Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 18:43:40 +0200 Subject: Move the "b" function to pycompat. --- sphinx/util/osutil.py | 2 +- sphinx/util/pycompat.py | 8 ++++++++ tests/test_markup.py | 8 +------- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py index 1010fb2f..250e7574 100644 --- a/sphinx/util/osutil.py +++ b/sphinx/util/osutil.py @@ -11,10 +11,10 @@ import os import re +import sys import time import errno import shutil -import sys from os import path # Errnos that we need. diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index 7bf768fa..8787a144 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -26,6 +26,14 @@ except NameError: base_exception = Exception +# the ubiquitous "bytes" helper function +if sys.version_info > (3, 0): + def b(s): + return s.encode('utf-8') +else: + b = str + + try: any = any all = all diff --git a/tests/test_markup.py b/tests/test_markup.py index 6d6badbb..092113bb 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -10,7 +10,6 @@ """ import re -import sys from util import * @@ -18,15 +17,10 @@ from docutils import frontend, utils, nodes from docutils.parsers import rst from sphinx.util import texescape +from sphinx.util.pycompat import b from sphinx.writers.html import HTMLWriter, SmartyPantsHTMLTranslator from sphinx.writers.latex import LaTeXWriter, LaTeXTranslator -if sys.version_info > (3, 0): - def b(s): - return s.encode('utf-8') -else: - b = str - def setup_module(): global app, settings, parser texescape.init() # otherwise done by the latex builder -- cgit v1.2.1 From bfa9e5142ba03a1462b5768a284df467331525a8 Mon Sep 17 00:00:00 2001 From: DasIch Date: Tue, 25 May 2010 00:55:30 +0200 Subject: fix line length --- sphinx/util/osutil.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py index 250e7574..9943b207 100644 --- a/sphinx/util/osutil.py +++ b/sphinx/util/osutil.py @@ -128,6 +128,7 @@ def make_filename(string): if sys.version_info < (3, 0): def ustrftime(format, *args): # strftime for unicode strings - return time.strftime(unicode(format).encode('utf-8'), *args).decode('utf-8') + return time.strftime(unicode(format).encode('utf-8'), *args) \ + .decode('utf-8') else: ustrftime = time.strftime -- cgit v1.2.1 From a2509af2998f41d31881b393110bb189b9afb36a Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 30 May 2010 02:07:53 +0200 Subject: Use .gettext() instead of .ugettext() on python3 --- sphinx/locale/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py index b0b89720..8df5f006 100644 --- a/sphinx/locale/__init__.py +++ b/sphinx/locale/__init__.py @@ -8,6 +8,7 @@ :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ +import sys import gettext import UserString @@ -178,8 +179,12 @@ pairindextypes = { translator = None -def _(message): - return translator.ugettext(message) +if sys.version_info >= (3, 0): + def _(message): + return translator.gettext(message) +else: + def _(message): + return translator.ugettext(message) def init(locale_dirs, language): global translator -- cgit v1.2.1 From 563c27d2d7e9e1e7092a0c273621ae08b54bb283 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 18:46:12 +0200 Subject: Nit. --- sphinx/locale/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py index 8df5f006..02958457 100644 --- a/sphinx/locale/__init__.py +++ b/sphinx/locale/__init__.py @@ -8,6 +8,7 @@ :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ + import sys import gettext import UserString -- cgit v1.2.1 From 4729bb07e8ae7040b3ca894183afde7165320a52 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 30 May 2010 02:28:57 +0200 Subject: Check if a string is not unicode as a workaround for 3.x --- sphinx/highlighting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/highlighting.py b/sphinx/highlighting.py index f5ea859c..c94405bc 100644 --- a/sphinx/highlighting.py +++ b/sphinx/highlighting.py @@ -175,7 +175,7 @@ class PygmentsBridge(object): return True def highlight_block(self, source, lang, linenos=False, warn=None): - if isinstance(source, str): + if not isinstance(source, unicode): source = source.decode() if not pygments: return self.unhighlighted(source) -- cgit v1.2.1 From 381fa7fe6cc6b83f0a2c5ca57ccdb27b303fe88f Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 30 May 2010 15:15:57 +0200 Subject: encode source code for parsing only on python 2.x --- sphinx/highlighting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/highlighting.py b/sphinx/highlighting.py index c94405bc..c168aeff 100644 --- a/sphinx/highlighting.py +++ b/sphinx/highlighting.py @@ -156,7 +156,7 @@ class PygmentsBridge(object): if sys.version_info >= (2, 5): src = 'from __future__ import with_statement\n' + src - if isinstance(src, unicode): + if sys.version_info < (3, 0) and isinstance(src, unicode): # Non-ASCII chars will only occur in string literals # and comments. If we wanted to give them to the parser # correctly, we'd have to find out the correct source -- cgit v1.2.1 From efd37f2b4583bd415702ad43d51b76ca6298a03e Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 30 May 2010 17:51:14 +0200 Subject: Fix encoding in config test and open configs in binary mode to warn for possible encoding errors --- sphinx/config.py | 2 +- tests/test_config.py | 5 +++-- tests/util.py | 11 +++++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/sphinx/config.py b/sphinx/config.py index 2ec76987..07c3d63a 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -165,7 +165,7 @@ class Config(object): try: try: os.chdir(dirname) - f = open(config_file, 'U') + f = open(config_file, 'Ub') try: code = compile(f.read(), config_file, 'exec') finally: diff --git a/tests/test_config.py b/tests/test_config.py index cb4e1105..23d92e39 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -84,11 +84,12 @@ def test_extension_values(app): @with_tempdir def test_errors_warnings(dir): # test the error for syntax errors in the config file - write_file(dir / 'conf.py', 'project = \n') + write_file(dir / 'conf.py', u'project = \n', 'ascii') raises_msg(ConfigError, 'conf.py', Config, dir, 'conf.py', {}, None) # test the warning for bytestrings with non-ascii content - write_file(dir / 'conf.py', '# -*- coding: latin-1\nproject = "foo\xe4"\n') + write_file(dir / 'conf.py', + u'# -*- coding: latin-1\nproject = "fooä"\n', 'latin-1') cfg = Config(dir, 'conf.py', {}, None) warned = [False] def warn(msg): diff --git a/tests/util.py b/tests/util.py index 1b24af0e..2cf4a775 100644 --- a/tests/util.py +++ b/tests/util.py @@ -11,6 +11,7 @@ import sys import StringIO import tempfile import shutil +from codecs import open try: from functools import wraps @@ -191,8 +192,14 @@ def with_tempdir(func): return new_func -def write_file(name, contents): - f = open(str(name), 'wb') +def write_file(name, contents, encoding=None): + if encoding is None: + mode = 'wb' + if isinstance(contents, unicode): + contents = contents.encode('ascii') + else: + mode = 'w' + f = open(str(name), 'wb', encoding=encoding) f.write(contents) f.close() -- cgit v1.2.1 From 861dd3c479f8a8515bc5182a694d3c8f234dce43 Mon Sep 17 00:00:00 2001 From: DasIch Date: Tue, 1 Jun 2010 18:10:06 +0200 Subject: Replaced the path module with my own version --- tests/path.py | 972 +++------------------------------------------------------- 1 file changed, 49 insertions(+), 923 deletions(-) diff --git a/tests/path.py b/tests/path.py index f27e58a9..36ab3a9a 100644 --- a/tests/path.py +++ b/tests/path.py @@ -1,952 +1,78 @@ -""" path.py - An object representing a path to a file or directory. - -Example: - -from path import path -d = path('/home/guido/bin') -for f in d.files('*.py'): - f.chmod(0755) - -This module requires Python 2.2 or later. - - -URL: http://www.jorendorff.com/articles/python/path -Author: Jason Orendorff (and others - see the url!) -Date: 9 Mar 2007 +#!/usr/bin/env python +# coding: utf-8 """ + path + ~~~~ + :copyright: Copyright 2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" +import os +import sys +import shutil +from codecs import open -# TODO -# - Tree-walking functions don't avoid symlink loops. Matt Harrison -# sent me a patch for this. -# - Bug in write_text(). It doesn't support Universal newline mode. -# - Better error message in listdir() when self isn't a -# directory. (On Windows, the error message really sucks.) -# - Make sure everything has a good docstring. -# - Add methods for regex find and replace. -# - guess_content_type() method? -# - Perhaps support arguments to touch(). - -from __future__ import generators - -import sys, warnings, os, fnmatch, glob, shutil, codecs - -__version__ = '2.2' -__all__ = ['path'] - -# Platform-specific support for path.owner -if os.name == 'nt': - try: - import win32security - except ImportError: - win32security = None -else: - try: - import pwd - except ImportError: - pwd = None - -# Pre-2.3 support. Are unicode filenames supported? -_base = str -_getcwd = os.getcwd -try: - if os.path.supports_unicode_filenames: - _base = unicode - _getcwd = os.getcwdu -except AttributeError: - pass - -# Pre-2.3 workaround for basestring. -try: - basestring -except NameError: - basestring = (str, unicode) - -# Universal newline support -_textmode = 'r' -try: - file -except NameError: - _textmode = 'U' -else: - if hasattr(file, 'newlines'): - _textmode = 'U' - - -class TreeWalkWarning(Warning): - pass - -class path(_base): - """ Represents a filesystem path. - - For documentation on individual methods, consult their - counterparts in os.path. - """ - - # --- Special Python methods. - - def __repr__(self): - return 'path(%s)' % _base.__repr__(self) - - # Adding a path and a string yields a path. - def __add__(self, more): - try: - resultStr = _base.__add__(self, more) - except TypeError: #Python bug - resultStr = NotImplemented - if resultStr is NotImplemented: - return resultStr - return self.__class__(resultStr) - - def __radd__(self, other): - if isinstance(other, basestring): - return self.__class__(other.__add__(self)) - else: - return NotImplemented - - # The / operator joins paths. - def __div__(self, rel): - """ fp.__div__(rel) == fp / rel == fp.joinpath(rel) - - Join two path components, adding a separator character if - needed. - """ - return self.__class__(os.path.join(self, rel)) - - # Make the / operator work even when true division is enabled. - __truediv__ = __div__ - - def getcwd(cls): - """ Return the current working directory as a path object. """ - return cls(_getcwd()) - getcwd = classmethod(getcwd) - - - # --- Operations on path strings. - - isabs = os.path.isabs - def abspath(self): return self.__class__(os.path.abspath(self)) - def normcase(self): return self.__class__(os.path.normcase(self)) - def normpath(self): return self.__class__(os.path.normpath(self)) - def realpath(self): return self.__class__(os.path.realpath(self)) - def expanduser(self): return self.__class__(os.path.expanduser(self)) - def expandvars(self): return self.__class__(os.path.expandvars(self)) - def dirname(self): return self.__class__(os.path.dirname(self)) - basename = os.path.basename - - def expand(self): - """ Clean up a filename by calling expandvars(), - expanduser(), and normpath() on it. - - This is commonly everything needed to clean up a filename - read from a configuration file, for example. - """ - return self.expandvars().expanduser().normpath() - - def _get_namebase(self): - base, ext = os.path.splitext(self.name) - return base - - def _get_ext(self): - f, ext = os.path.splitext(_base(self)) - return ext - - def _get_drive(self): - drive, r = os.path.splitdrive(self) - return self.__class__(drive) - - parent = property( - dirname, None, None, - """ This path's parent directory, as a new path object. - - For example, path('/usr/local/lib/libpython.so').parent == path('/usr/local/lib') - """) - - name = property( - basename, None, None, - """ The name of this file or directory without the full path. - - For example, path('/usr/local/lib/libpython.so').name == 'libpython.so' - """) - - namebase = property( - _get_namebase, None, None, - """ The same as path.name, but with one file extension stripped off. - - For example, path('/home/guido/python.tar.gz').name == 'python.tar.gz', - but path('/home/guido/python.tar.gz').namebase == 'python.tar' - """) - - ext = property( - _get_ext, None, None, - """ The file extension, for example '.py'. """) - - drive = property( - _get_drive, None, None, - """ The drive specifier, for example 'C:'. - This is always empty on systems that don't use drive specifiers. - """) - - def splitpath(self): - """ p.splitpath() -> Return (p.parent, p.name). """ - parent, child = os.path.split(self) - return self.__class__(parent), child - - def splitdrive(self): - """ p.splitdrive() -> Return (p.drive, ). - - Split the drive specifier from this path. If there is - no drive specifier, p.drive is empty, so the return value - is simply (path(''), p). This is always the case on Unix. - """ - drive, rel = os.path.splitdrive(self) - return self.__class__(drive), rel - - def splitext(self): - """ p.splitext() -> Return (p.stripext(), p.ext). - - Split the filename extension from this path and return - the two parts. Either part may be empty. - - The extension is everything from '.' to the end of the - last path segment. This has the property that if - (a, b) == p.splitext(), then a + b == p. - """ - filename, ext = os.path.splitext(self) - return self.__class__(filename), ext - - def stripext(self): - """ p.stripext() -> Remove one file extension from the path. - - For example, path('/home/guido/python.tar.gz').stripext() - returns path('/home/guido/python.tar'). - """ - return self.splitext()[0] - - if hasattr(os.path, 'splitunc'): - def splitunc(self): - unc, rest = os.path.splitunc(self) - return self.__class__(unc), rest - - def _get_uncshare(self): - unc, r = os.path.splitunc(self) - return self.__class__(unc) - - uncshare = property( - _get_uncshare, None, None, - """ The UNC mount point for this path. - This is empty for paths on local drives. """) - - def joinpath(self, *args): - """ Join two or more path components, adding a separator - character (os.sep) if needed. Returns a new path - object. - """ - return self.__class__(os.path.join(self, *args)) - - def splitall(self): - r""" Return a list of the path components in this path. - - The first item in the list will be a path. Its value will be - either os.curdir, os.pardir, empty, or the root directory of - this path (for example, '/' or 'C:\\'). The other items in - the list will be strings. - - path.path.joinpath(*result) will yield the original path. - """ - parts = [] - loc = self - while loc != os.curdir and loc != os.pardir: - prev = loc - loc, child = prev.splitpath() - if loc == prev: - break - parts.append(child) - parts.append(loc) - parts.reverse() - return parts - - def relpath(self): - """ Return this path as a relative path, - based from the current working directory. - """ - cwd = self.__class__(os.getcwd()) - return cwd.relpathto(self) - - def relpathto(self, dest): - """ Return a relative path from self to dest. - - If there is no relative path from self to dest, for example if - they reside on different drives in Windows, then this returns - dest.abspath(). - """ - origin = self.abspath() - dest = self.__class__(dest).abspath() - - orig_list = origin.normcase().splitall() - # Don't normcase dest! We want to preserve the case. - dest_list = dest.splitall() - - if orig_list[0] != os.path.normcase(dest_list[0]): - # Can't get here from there. - return dest - - # Find the location where the two paths start to differ. - i = 0 - for start_seg, dest_seg in zip(orig_list, dest_list): - if start_seg != os.path.normcase(dest_seg): - break - i += 1 - - # Now i is the point where the two paths diverge. - # Need a certain number of "os.pardir"s to work up - # from the origin to the point of divergence. - segments = [os.pardir] * (len(orig_list) - i) - # Need to add the diverging part of dest_list. - segments += dest_list[i:] - if len(segments) == 0: - # If they happen to be identical, use os.curdir. - relpath = os.curdir - else: - relpath = os.path.join(*segments) - return self.__class__(relpath) - - # --- Listing, searching, walking, and matching - - def listdir(self, pattern=None): - """ D.listdir() -> List of items in this directory. - - Use D.files() or D.dirs() instead if you want a listing - of just files or just subdirectories. - - The elements of the list are path objects. - - With the optional 'pattern' argument, this only lists - items whose names match the given pattern. - """ - names = os.listdir(self) - if pattern is not None: - names = fnmatch.filter(names, pattern) - return [self / child for child in names] - - def dirs(self, pattern=None): - """ D.dirs() -> List of this directory's subdirectories. - - The elements of the list are path objects. - This does not walk recursively into subdirectories - (but see path.walkdirs). - - With the optional 'pattern' argument, this only lists - directories whose names match the given pattern. For - example, d.dirs('build-*'). - """ - return [p for p in self.listdir(pattern) if p.isdir()] - - def files(self, pattern=None): - """ D.files() -> List of the files in this directory. - - The elements of the list are path objects. - This does not walk into subdirectories (see path.walkfiles). - - With the optional 'pattern' argument, this only lists files - whose names match the given pattern. For example, - d.files('*.pyc'). - """ - - return [p for p in self.listdir(pattern) if p.isfile()] - - def walk(self, pattern=None, errors='strict'): - """ D.walk() -> iterator over files and subdirs, recursively. - - The iterator yields path objects naming each child item of - this directory and its descendants. This requires that - D.isdir(). - - This performs a depth-first traversal of the directory tree. - Each directory is returned just before all its children. - - The errors= keyword argument controls behavior when an - error occurs. The default is 'strict', which causes an - exception. The other allowed values are 'warn', which - reports the error via warnings.warn(), and 'ignore'. - """ - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") - - try: - childList = self.listdir() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise - - for child in childList: - if pattern is None or child.fnmatch(pattern): - yield child - try: - isdir = child.isdir() - except Exception: - if errors == 'ignore': - isdir = False - elif errors == 'warn': - warnings.warn( - "Unable to access '%s': %s" - % (child, sys.exc_info()[1]), - TreeWalkWarning) - isdir = False - else: - raise - - if isdir: - for item in child.walk(pattern, errors): - yield item - - def walkdirs(self, pattern=None, errors='strict'): - """ D.walkdirs() -> iterator over subdirs, recursively. - - With the optional 'pattern' argument, this yields only - directories whose names match the given pattern. For - example, mydir.walkdirs('*test') yields only directories - with names ending in 'test'. - - The errors= keyword argument controls behavior when an - error occurs. The default is 'strict', which causes an - exception. The other allowed values are 'warn', which - reports the error via warnings.warn(), and 'ignore'. - """ - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") - - try: - dirs = self.dirs() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise - - for child in dirs: - if pattern is None or child.fnmatch(pattern): - yield child - for subsubdir in child.walkdirs(pattern, errors): - yield subsubdir - - def walkfiles(self, pattern=None, errors='strict'): - """ D.walkfiles() -> iterator over files in D, recursively. - The optional argument, pattern, limits the results to files - with names that match the pattern. For example, - mydir.walkfiles('*.tmp') yields only files with the .tmp - extension. - """ - if errors not in ('strict', 'warn', 'ignore'): - raise ValueError("invalid errors parameter") +FILESYSTEMENCODING = sys.getfilesystemencoding() or sys.getdefaultencoding() - try: - childList = self.listdir() - except Exception: - if errors == 'ignore': - return - elif errors == 'warn': - warnings.warn( - "Unable to list directory '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - return - else: - raise - for child in childList: - try: - isfile = child.isfile() - isdir = not isfile and child.isdir() - except: - if errors == 'ignore': - continue - elif errors == 'warn': - warnings.warn( - "Unable to access '%s': %s" - % (self, sys.exc_info()[1]), - TreeWalkWarning) - continue +class path(str): + if sys.version_info < (3, 0): + def __new__(cls, s, encoding=FILESYSTEMENCODING, errors=None): + if isinstance(s, unicode): + if errors is None: + s = s.encode(encoding) else: - raise - - if isfile: - if pattern is None or child.fnmatch(pattern): - yield child - elif isdir: - for f in child.walkfiles(pattern, errors): - yield f + s = s.encode(encoding, errors=errors) + return str.__new__(cls, s) + return str.__new__(cls, s) - def fnmatch(self, pattern): - """ Return True if self.name matches the given pattern. + @property + def parent(self): + return self.__class__(os.path.dirname(self)) - pattern - A filename pattern with wildcards, - for example '*.py'. - """ - return fnmatch.fnmatch(self.name, pattern) + def abspath(self): + return self.__class__(os.path.abspath(self)) - def glob(self, pattern): - """ Return a list of path objects that match the pattern. + def isdir(self): + return os.path.isdir(self) - pattern - a path relative to this directory, with wildcards. + def isfile(self): + return os.path.isfile(self) - For example, path('/users').glob('*/bin/*') returns a list - of all the files users have in their bin directories. - """ - cls = self.__class__ - return [cls(s) for s in glob.glob(_base(self / pattern))] + def rmtree(self, ignore_errors=False, onerror=None): + shutil.rmtree(self, ignore_errors=ignore_errors, onerror=onerror) + def copytree(self, destination, symlinks=False, ignore=None): + shutil.copytree(self, destination, symlinks=symlinks, ignore=ignore) - # --- Reading or writing an entire file at once. - - def open(self, mode='r'): - """ Open this file. Return a file object. """ - return open(self, mode) - - def bytes(self): - """ Open this file, read all bytes, return them as a string. """ - f = self.open('rb') - try: - return f.read() - finally: - f.close() - - def write_bytes(self, bytes, append=False): - """ Open this file and write the given bytes to it. + def unlink(self): + os.unlink(self) - Default behavior is to overwrite any existing file. - Call p.write_bytes(bytes, append=True) to append instead. - """ - if append: - mode = 'ab' - else: - mode = 'wb' - f = self.open(mode) + def write_text(self, text, **kwargs): + f = open(self, 'w', **kwargs) try: - f.write(bytes) + f.write(text) finally: f.close() - def text(self, encoding=None, errors='strict'): - r""" Open this file, read it in, return the content as a string. - - This uses 'U' mode in Python 2.3 and later, so '\r\n' and '\r' - are automatically translated to '\n'. - - Optional arguments: - - encoding - The Unicode encoding (or character set) of - the file. If present, the content of the file is - decoded and returned as a unicode object; otherwise - it is returned as an 8-bit str. - errors - How to handle Unicode errors; see help(str.decode) - for the options. Default is 'strict'. - """ - if encoding is None: - # 8-bit - f = self.open(_textmode) - try: - return f.read() - finally: - f.close() - else: - # Unicode - f = codecs.open(self, 'r', encoding, errors) - # (Note - Can't use 'U' mode here, since codecs.open - # doesn't support 'U' mode, even in Python 2.3.) - try: - t = f.read() - finally: - f.close() - return (t.replace(u'\r\n', u'\n') - .replace(u'\r\x85', u'\n') - .replace(u'\r', u'\n') - .replace(u'\x85', u'\n') - .replace(u'\u2028', u'\n')) - - def write_text(self, text, encoding=None, errors='strict', linesep=os.linesep, append=False): - r""" Write the given text to this file. - - The default behavior is to overwrite any existing file; - to append instead, use the 'append=True' keyword argument. - - There are two differences between path.write_text() and - path.write_bytes(): newline handling and Unicode handling. - See below. - - Parameters: - - - text - str/unicode - The text to be written. - - - encoding - str - The Unicode encoding that will be used. - This is ignored if 'text' isn't a Unicode string. - - - errors - str - How to handle Unicode encoding errors. - Default is 'strict'. See help(unicode.encode) for the - options. This is ignored if 'text' isn't a Unicode - string. - - - linesep - keyword argument - str/unicode - The sequence of - characters to be used to mark end-of-line. The default is - os.linesep. You can also specify None; this means to - leave all newlines as they are in 'text'. - - - append - keyword argument - bool - Specifies what to do if - the file already exists (True: append to the end of it; - False: overwrite it.) The default is False. - - - --- Newline handling. - - write_text() converts all standard end-of-line sequences - ('\n', '\r', and '\r\n') to your platform's default end-of-line - sequence (see os.linesep; on Windows, for example, the - end-of-line marker is '\r\n'). - - If you don't like your platform's default, you can override it - using the 'linesep=' keyword argument. If you specifically want - write_text() to preserve the newlines as-is, use 'linesep=None'. - - This applies to Unicode text the same as to 8-bit text, except - there are three additional standard Unicode end-of-line sequences: - u'\x85', u'\r\x85', and u'\u2028'. - - (This is slightly different from when you open a file for - writing with fopen(filename, "w") in C or file(filename, 'w') - in Python.) - - - --- Unicode - - If 'text' isn't Unicode, then apart from newline handling, the - bytes are written verbatim to the file. The 'encoding' and - 'errors' arguments are not used and must be omitted. - - If 'text' is Unicode, it is first converted to bytes using the - specified 'encoding' (or the default encoding if 'encoding' - isn't specified). The 'errors' argument applies only to this - conversion. - - """ - if isinstance(text, unicode): - if linesep is not None: - # Convert all standard end-of-line sequences to - # ordinary newline characters. - text = (text.replace(u'\r\n', u'\n') - .replace(u'\r\x85', u'\n') - .replace(u'\r', u'\n') - .replace(u'\x85', u'\n') - .replace(u'\u2028', u'\n')) - text = text.replace(u'\n', linesep) - if encoding is None: - encoding = sys.getdefaultencoding() - bytes = text.encode(encoding, errors) - else: - # It is an error to specify an encoding if 'text' is - # an 8-bit string. - assert encoding is None - - if linesep is not None: - text = (text.replace('\r\n', '\n') - .replace('\r', '\n')) - bytes = text.replace('\n', linesep) - - self.write_bytes(bytes, append) - - def lines(self, encoding=None, errors='strict', retain=True): - r""" Open this file, read all lines, return them in a list. - - Optional arguments: - encoding - The Unicode encoding (or character set) of - the file. The default is None, meaning the content - of the file is read as 8-bit characters and returned - as a list of (non-Unicode) str objects. - errors - How to handle Unicode errors; see help(str.decode) - for the options. Default is 'strict' - retain - If true, retain newline characters; but all newline - character combinations ('\r', '\n', '\r\n') are - translated to '\n'. If false, newline characters are - stripped off. Default is True. - - This uses 'U' mode in Python 2.3 and later. - """ - if encoding is None and retain: - f = self.open(_textmode) - try: - return f.readlines() - finally: - f.close() - else: - return self.text(encoding, errors).splitlines(retain) - - def write_lines(self, lines, encoding=None, errors='strict', - linesep=os.linesep, append=False): - r""" Write the given lines of text to this file. - - By default this overwrites any existing file at this path. - - This puts a platform-specific newline sequence on every line. - See 'linesep' below. - - lines - A list of strings. - - encoding - A Unicode encoding to use. This applies only if - 'lines' contains any Unicode strings. - - errors - How to handle errors in Unicode encoding. This - also applies only to Unicode strings. - - linesep - The desired line-ending. This line-ending is - applied to every line. If a line already has any - standard line ending ('\r', '\n', '\r\n', u'\x85', - u'\r\x85', u'\u2028'), that will be stripped off and - this will be used instead. The default is os.linesep, - which is platform-dependent ('\r\n' on Windows, '\n' on - Unix, etc.) Specify None to write the lines as-is, - like file.writelines(). - - Use the keyword argument append=True to append lines to the - file. The default is to overwrite the file. Warning: - When you use this with Unicode data, if the encoding of the - existing data in the file is different from the encoding - you specify with the encoding= parameter, the result is - mixed-encoding data, which can really confuse someone trying - to read the file later. - """ - if append: - mode = 'ab' - else: - mode = 'wb' - f = self.open(mode) + def text(self, **kwargs): + f = open(self, mode='U', **kwargs) try: - for line in lines: - isUnicode = isinstance(line, unicode) - if linesep is not None: - # Strip off any existing line-end and add the - # specified linesep string. - if isUnicode: - if line[-2:] in (u'\r\n', u'\x0d\x85'): - line = line[:-2] - elif line[-1:] in (u'\r', u'\n', - u'\x85', u'\u2028'): - line = line[:-1] - else: - if line[-2:] == '\r\n': - line = line[:-2] - elif line[-1:] in ('\r', '\n'): - line = line[:-1] - line += linesep - if isUnicode: - if encoding is None: - encoding = sys.getdefaultencoding() - line = line.encode(encoding, errors) - f.write(line) + return f.read() finally: f.close() - # --- Methods for querying the filesystem. - - exists = os.path.exists - isdir = os.path.isdir - isfile = os.path.isfile - islink = os.path.islink - ismount = os.path.ismount - - if hasattr(os.path, 'samefile'): - samefile = os.path.samefile - - getatime = os.path.getatime - atime = property( - getatime, None, None, - """ Last access time of the file. """) - - getmtime = os.path.getmtime - mtime = property( - getmtime, None, None, - """ Last-modified time of the file. """) - - if hasattr(os.path, 'getctime'): - getctime = os.path.getctime - ctime = property( - getctime, None, None, - """ Creation time of the file. """) - - getsize = os.path.getsize - size = property( - getsize, None, None, - """ Size of the file, in bytes. """) - - if hasattr(os, 'access'): - def access(self, mode): - """ Return true if current user has access to this path. - - mode - One of the constants os.F_OK, os.R_OK, os.W_OK, os.X_OK - """ - return os.access(self, mode) - - def stat(self): - """ Perform a stat() system call on this path. """ - return os.stat(self) - - def lstat(self): - """ Like path.stat(), but do not follow symbolic links. """ - return os.lstat(self) - - def get_owner(self): - r""" Return the name of the owner of this file or directory. - - This follows symbolic links. - - On Windows, this returns a name of the form ur'DOMAIN\User Name'. - On Windows, a group can own a file or directory. - """ - if os.name == 'nt': - if win32security is None: - raise Exception("path.owner requires win32all to be installed") - desc = win32security.GetFileSecurity( - self, win32security.OWNER_SECURITY_INFORMATION) - sid = desc.GetSecurityDescriptorOwner() - account, domain, typecode = win32security.LookupAccountSid(None, sid) - return domain + u'\\' + account - else: - if pwd is None: - raise NotImplementedError("path.owner is not implemented on this platform.") - st = self.stat() - return pwd.getpwuid(st.st_uid).pw_name - - owner = property( - get_owner, None, None, - """ Name of the owner of this file or directory. """) - - if hasattr(os, 'statvfs'): - def statvfs(self): - """ Perform a statvfs() system call on this path. """ - return os.statvfs(self) - - if hasattr(os, 'pathconf'): - def pathconf(self, name): - return os.pathconf(self, name) - - - # --- Modifying operations on files and directories - - def utime(self, times): - """ Set the access and modified times of this file. """ - os.utime(self, times) - - def chmod(self, mode): - os.chmod(self, mode) - - if hasattr(os, 'chown'): - def chown(self, uid, gid): - os.chown(self, uid, gid) - - def rename(self, new): - os.rename(self, new) - - def renames(self, new): - os.renames(self, new) - - - # --- Create/delete operations on directories - - def mkdir(self, mode=0777): - os.mkdir(self, mode) + def exists(self): + return os.path.exists(self) def makedirs(self, mode=0777): os.makedirs(self, mode) - def rmdir(self): - os.rmdir(self) - - def removedirs(self): - os.removedirs(self) - - - # --- Modifying operations on files - - def touch(self): - """ Set the access/modified times of this file to the current time. - Create the file if it does not exist. - """ - fd = os.open(self, os.O_WRONLY | os.O_CREAT, 0666) - os.close(fd) - os.utime(self, None) - - def remove(self): - os.remove(self) - - def unlink(self): - os.unlink(self) - - - # --- Links - - if hasattr(os, 'link'): - def link(self, newpath): - """ Create a hard link at 'newpath', pointing to this file. """ - os.link(self, newpath) - - if hasattr(os, 'symlink'): - def symlink(self, newlink): - """ Create a symbolic link at 'newlink', pointing here. """ - os.symlink(self, newlink) - - if hasattr(os, 'readlink'): - def readlink(self): - """ Return the path to which this symbolic link points. - - The result may be an absolute or a relative path. - """ - return self.__class__(os.readlink(self)) - - def readlinkabs(self): - """ Return the path to which this symbolic link points. - - The result is always an absolute path. - """ - p = self.readlink() - if p.isabs(): - return p - else: - return (self.parent / p).abspath() - - - # --- High-level functions from shutil - - copyfile = shutil.copyfile - copymode = shutil.copymode - copystat = shutil.copystat - copy = shutil.copy - copy2 = shutil.copy2 - copytree = shutil.copytree - if hasattr(shutil, 'move'): - move = shutil.move - rmtree = shutil.rmtree - - - # --- Special stuff from os - - if hasattr(os, 'chroot'): - def chroot(self): - os.chroot(self) + def joinpath(self, *args): + return self.__class__(os.path.join(self, *map(self.__class__, args))) - if hasattr(os, 'startfile'): - def startfile(self): - os.startfile(self) + __div__ = __truediv__ = joinpath + def __repr__(self): + return '%s(%s)' % (self.__class__.__name__, str.__repr__(self)) -- cgit v1.2.1 From 122a47304bb1d3960da7decc64a9f28291047ed9 Mon Sep 17 00:00:00 2001 From: DasIch Date: Tue, 1 Jun 2010 19:59:38 +0200 Subject: Added a movetree method to the path object to make it more consistent along with documentation for every method of it. --- tests/path.py | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/tests/path.py b/tests/path.py index 36ab3a9a..dccdac3a 100644 --- a/tests/path.py +++ b/tests/path.py @@ -17,6 +17,9 @@ FILESYSTEMENCODING = sys.getfilesystemencoding() or sys.getdefaultencoding() class path(str): + """ + Represents a path which behaves like a string. + """ if sys.version_info < (3, 0): def __new__(cls, s, encoding=FILESYSTEMENCODING, errors=None): if isinstance(s, unicode): @@ -29,27 +32,103 @@ class path(str): @property def parent(self): + """ + The name of the directory the file or directory is in. + """ return self.__class__(os.path.dirname(self)) def abspath(self): + """ + Returns the absolute path. + """ return self.__class__(os.path.abspath(self)) + def isabs(self): + """ + Returns ``True`` if the path is absolute. + """ + return os.path.isabs(self) + def isdir(self): + """ + Returns ``True`` if the path is a directory. + """ return os.path.isdir(self) def isfile(self): + """ + Returns ``True`` if the path is a file. + """ return os.path.isfile(self) + def islink(self): + """ + Returns ``True`` if the path is a symbolic link. + """ + return os.path.islink(self) + + def ismount(self): + """ + Returns ``True`` if the path is a mount point. + """ + return os.path.ismount(self) + def rmtree(self, ignore_errors=False, onerror=None): + """ + Removes the file or directory and any files or directories it may + contain. + + :param ignore_errors: + If ``True`` errors are silently ignored, otherwise an exception + is raised in case an error occurs. + + :param onerror: + A callback which gets called with the arguments `func`, `path` and + `exc_info`. `func` is one of :func:`os.listdir`, :func:`os.remove` + or :func:`os.rmdir`. `path` is the argument to the function which + caused it to fail and `exc_info` is a tuple as returned by + :func:`sys.exc_info`. + """ shutil.rmtree(self, ignore_errors=ignore_errors, onerror=onerror) def copytree(self, destination, symlinks=False, ignore=None): + """ + Recursively copy a directory to the given `destination`. If the given + `destination` does not exist it will be created. + + :param symlinks: + If ``True`` symbolic links in the source tree result in symbolic + links in the destination tree otherwise the contents of the files + pointed to by the symbolic links are copied. + + :param ignore: + A callback which gets called with the path of the directory being + copied and a list of paths as returned by :func:`os.listdir`. + """ shutil.copytree(self, destination, symlinks=symlinks, ignore=ignore) + def movetree(self, destination): + """ + Recursively move the file or directory to the given `destination` + similar to the Unix "mv" command. + + If the `destination` is a file it may be overwritten depending on the + :func:`os.rename` semantics. + """ + shutil.move(self, destination) + + move = movetree + def unlink(self): + """ + Removes a file. + """ os.unlink(self) def write_text(self, text, **kwargs): + """ + Writes the given `text` to the file. + """ f = open(self, 'w', **kwargs) try: f.write(text) @@ -57,6 +136,9 @@ class path(str): f.close() def text(self, **kwargs): + """ + Returns the text in the file. + """ f = open(self, mode='U', **kwargs) try: return f.read() @@ -64,12 +146,28 @@ class path(str): f.close() def exists(self): + """ + Returns ``True`` if the path exist. + """ return os.path.exists(self) + def lexists(self): + """ + Returns ``True`` if the path exists unless it is a broken symbolic + link. + """ + return os.path.lexists(self) + def makedirs(self, mode=0777): + """ + Recursively create directories. + """ os.makedirs(self, mode) def joinpath(self, *args): + """ + Joins the path with the argument given and returns the result. + """ return self.__class__(os.path.join(self, *map(self.__class__, args))) __div__ = __truediv__ = joinpath -- cgit v1.2.1 From c15e9ef0ff35686578d8570ece99611a9b23e137 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 6 Jun 2010 23:13:06 +0200 Subject: don't use string.strip anymore --- tests/test_autosummary.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_autosummary.py b/tests/test_autosummary.py index 7e309367..20fb06e0 100644 --- a/tests/test_autosummary.py +++ b/tests/test_autosummary.py @@ -9,8 +9,6 @@ :license: BSD, see LICENSE for details. """ -import string - from util import * from sphinx.ext.autosummary import mangle_signature @@ -27,7 +25,7 @@ def test_mangle_signature(): (a, b, c='foobar()', d=123) :: (a, b[, c, d]) """ - TEST = [map(string.strip, x.split("::")) for x in TEST.split("\n") + TEST = [map(lambda x: x.strip(), x.split("::")) for x in TEST.split("\n") if '::' in x] for inp, outp in TEST: res = mangle_signature(inp).strip().replace(u"\u00a0", " ") -- cgit v1.2.1 From e6c4415f20e27c7b8bfa20189df0afd052156202 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 18:51:57 +0200 Subject: Use next() function instead of iter.next(). --- sphinx/pycode/__init__.py | 3 ++- sphinx/util/pycompat.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/sphinx/pycode/__init__.py b/sphinx/pycode/__init__.py index cb9c0887..ef92297c 100644 --- a/sphinx/pycode/__init__.py +++ b/sphinx/pycode/__init__.py @@ -18,6 +18,7 @@ from sphinx.errors import PycodeError from sphinx.pycode import nodes from sphinx.pycode.pgen2 import driver, token, tokenize, parse, literals from sphinx.util import get_module_source +from sphinx.util.pycompat import next from sphinx.util.docstrings import prepare_docstring, prepare_commentdoc @@ -279,7 +280,7 @@ class ModuleAnalyzer(object): result[fullname] = (dtype, startline, endline) expect_indent = False if tok in ('def', 'class'): - name = tokeniter.next()[1] + name = next(tokeniter)[1] namespace.append(name) fullname = '.'.join(namespace) stack.append((tok, fullname, spos[0], indent)) diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index 8787a144..365cd703 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -34,6 +34,14 @@ else: b = str +try: + next +except NameError: + # this is on Python 2, where the method is called "next" + def next(iterator): + return iterator.next() + + try: any = any all = all -- cgit v1.2.1 From c3911a3a03d5ef5e2eb588823ea142b3c6b5f0ad Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 19:08:44 +0200 Subject: Fix assignment. --- sphinx/util/pycompat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index 365cd703..0725545d 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -35,7 +35,7 @@ else: try: - next + next = next except NameError: # this is on Python 2, where the method is called "next" def next(iterator): -- cgit v1.2.1 From c620a7e4f9584a96140d392b4f30ba1237f08162 Mon Sep 17 00:00:00 2001 From: DasIch Date: Mon, 7 Jun 2010 00:44:09 +0200 Subject: open file in binary mode to get byte strings on python3 --- sphinx/directives/code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/directives/code.py b/sphinx/directives/code.py index 0647daf0..54d8edf1 100644 --- a/sphinx/directives/code.py +++ b/sphinx/directives/code.py @@ -119,7 +119,7 @@ class LiteralInclude(Directive): encoding = self.options.get('encoding', env.config.source_encoding) codec_info = codecs.lookup(encoding) try: - f = codecs.StreamReaderWriter(open(fn, 'U'), + f = codecs.StreamReaderWriter(open(fn, 'Ub'), codec_info[2], codec_info[3], 'strict') lines = f.readlines() f.close() -- cgit v1.2.1 From 05a6f46d3e182991431e3cd277b7b51ea813dadc Mon Sep 17 00:00:00 2001 From: DasIch Date: Mon, 7 Jun 2010 01:34:01 +0200 Subject: fixed file handling and parsing in intersphinx so it properly handles encodings --- sphinx/ext/intersphinx.py | 27 +++++++++++++++++++-------- tests/test_intersphinx.py | 17 ++++++++++------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index fb1f0e4f..31de2315 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -28,12 +28,20 @@ import time import zlib import urllib2 import posixpath +import codecs +import sys from os import path from docutils import nodes from sphinx.builders.html import INVENTORY_FILENAME +if sys.version_info >= (3, 0): + def b(s): + return s.encode('utf-8') +else: + b = str + handlers = [urllib2.ProxyHandler(), urllib2.HTTPRedirectHandler(), urllib2.HTTPHandler()] try: @@ -43,11 +51,14 @@ except NameError: urllib2.install_opener(urllib2.build_opener(*handlers)) +UTF8StreamReader = codecs.lookup('utf-8')[2] + def read_inventory_v1(f, uri, join): + f = UTF8StreamReader(f) invdata = {} line = f.next() - projname = line.rstrip()[11:].decode('utf-8') + projname = line.rstrip()[11:] line = f.next() version = line.rstrip()[11:] for line in f: @@ -70,25 +81,25 @@ def read_inventory_v2(f, uri, join, bufsize=16*1024): projname = line.rstrip()[11:].decode('utf-8') line = f.readline() version = line.rstrip()[11:].decode('utf-8') - line = f.readline() + line = f.readline().decode('utf-8') if 'zlib' not in line: raise ValueError def read_chunks(): decompressor = zlib.decompressobj() - for chunk in iter(lambda: f.read(bufsize), ''): + for chunk in iter(lambda: f.read(bufsize), b('')): yield decompressor.decompress(chunk) yield decompressor.flush() def split_lines(iter): - buf = '' + buf = b('') for chunk in iter: buf += chunk - lineend = buf.find('\n') + lineend = buf.find(b('\n')) while lineend != -1: yield buf[:lineend].decode('utf-8') buf = buf[lineend+1:] - lineend = buf.find('\n') + lineend = buf.find(b('\n')) assert not buf for line in split_lines(read_chunks()): @@ -111,13 +122,13 @@ def fetch_inventory(app, uri, inv): if inv.find('://') != -1: f = urllib2.urlopen(inv) else: - f = open(path.join(app.srcdir, inv)) + f = open(path.join(app.srcdir, inv), 'rb') except Exception, err: app.warn('intersphinx inventory %r not fetchable due to ' '%s: %s' % (inv, err.__class__, err)) return try: - line = f.readline().rstrip() + line = f.readline().rstrip().decode('utf-8') try: if line == '# Sphinx inventory version 1': invdata = read_inventory_v1(f, uri, join) diff --git a/tests/test_intersphinx.py b/tests/test_intersphinx.py index 8b6547e5..4f70bd20 100644 --- a/tests/test_intersphinx.py +++ b/tests/test_intersphinx.py @@ -11,7 +11,10 @@ import zlib import posixpath -from cStringIO import StringIO +try: + from io import BytesIO +except ImportError: + from cStringIO import StringIO as BytesIO from docutils import nodes @@ -28,23 +31,23 @@ inventory_v1 = '''\ # Version: 1.0 module mod foo.html module.cls class foo.html -''' +'''.encode('utf-8') inventory_v2 = '''\ # Sphinx inventory version 2 # Project: foo # Version: 2.0 # The remainder of this file is compressed with zlib. -''' + zlib.compress('''\ +'''.encode('utf-8') + zlib.compress('''\ module1 py:module 0 foo.html#module-module1 Long Module desc module2 py:module 0 foo.html#module-$ - module1.func py:function 1 sub/foo.html#$ - CFunc c:function 2 cfunc.html#CFunc - -''') +'''.encode('utf-8')) def test_read_inventory_v1(): - f = StringIO(inventory_v1) + f = BytesIO(inventory_v1) f.readline() invdata = read_inventory_v1(f, '/util', posixpath.join) assert invdata['py:module']['module'] == \ @@ -54,12 +57,12 @@ def test_read_inventory_v1(): def test_read_inventory_v2(): - f = StringIO(inventory_v2) + f = BytesIO(inventory_v2) f.readline() invdata1 = read_inventory_v2(f, '/util', posixpath.join) # try again with a small buffer size to test the chunking algorithm - f = StringIO(inventory_v2) + f = BytesIO(inventory_v2) f.readline() invdata2 = read_inventory_v2(f, '/util', posixpath.join, bufsize=5) -- cgit v1.2.1 From 4c0d52c270e93a753fd1f9dafba965cf74fd5c9a Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 18:54:24 +0200 Subject: Use b() from pycompat. --- sphinx/ext/intersphinx.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 31de2315..07ee24e3 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -26,21 +26,16 @@ import time import zlib +import codecs import urllib2 import posixpath -import codecs -import sys from os import path from docutils import nodes from sphinx.builders.html import INVENTORY_FILENAME +from sphinx.util.pycompat import b -if sys.version_info >= (3, 0): - def b(s): - return s.encode('utf-8') -else: - b = str handlers = [urllib2.ProxyHandler(), urllib2.HTTPRedirectHandler(), urllib2.HTTPHandler()] -- cgit v1.2.1 From c529f967eca0e62cffa3fc929a04e7fe258d6a6d Mon Sep 17 00:00:00 2001 From: DasIch Date: Sat, 12 Jun 2010 15:16:57 +0200 Subject: Copy converted tests to build/lib/tests not build/lib/test --- tests/run.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/run.py b/tests/run.py index 5fd30d62..f384fe26 100755 --- a/tests/run.py +++ b/tests/run.py @@ -14,10 +14,10 @@ import sys from os import path, chdir if sys.version_info >= (3,): - print('Copying and converting sources to build/lib/test...') + print('Copying and converting sources to build/lib/tests...') from distutils.util import copydir_run_2to3 testroot = path.dirname(__file__) or '.' - newroot = path.join(testroot, path.pardir, 'build', 'lib', 'test') + newroot = path.join(testroot, path.pardir, 'build', 'lib', 'tests') copydir_run_2to3(testroot, newroot) # switch to the converted dir so nose tests the right tests chdir(newroot) -- cgit v1.2.1 From 8232e3a896fb23e71c11c0d41901a50a36c4690c Mon Sep 17 00:00:00 2001 From: DasIch Date: Sat, 12 Jun 2010 20:21:14 +0200 Subject: Remove unnecessary code --- tests/path.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/path.py b/tests/path.py index dccdac3a..28a8c22a 100644 --- a/tests/path.py +++ b/tests/path.py @@ -21,12 +21,9 @@ class path(str): Represents a path which behaves like a string. """ if sys.version_info < (3, 0): - def __new__(cls, s, encoding=FILESYSTEMENCODING, errors=None): + def __new__(cls, s, encoding=FILESYSTEMENCODING, errors='strict'): if isinstance(s, unicode): - if errors is None: - s = s.encode(encoding) - else: - s = s.encode(encoding, errors=errors) + s = s.encode(encoding, errors=errors) return str.__new__(cls, s) return str.__new__(cls, s) -- cgit v1.2.1 From 5ed4e6e6798a7f2c8c7eb7facd670639e162786b Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 18:55:39 +0200 Subject: Pass document name as bytes. --- sphinx/builders/html.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index 8d96c146..c5f7312a 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -35,7 +35,7 @@ from sphinx.util.osutil import SEP, os_path, relative_uri, ensuredir, \ movefile, ustrftime, copyfile from sphinx.util.nodes import inline_all_toctrees from sphinx.util.matching import patmatch, compile_matchers -from sphinx.util.pycompat import any +from sphinx.util.pycompat import any, b from sphinx.errors import SphinxError from sphinx.locale import _ from sphinx.search import js_index @@ -199,7 +199,7 @@ class StandaloneHTMLBuilder(Builder): """Utility: Render a lone doctree node.""" if node is None: return {'fragment': ''} - doc = new_document('') + doc = new_document(b('')) doc.append(node) if self._publisher is None: -- cgit v1.2.1 From 28819144db7761896ba1008ce50ee904256e477c Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 13 Jun 2010 23:08:32 +0200 Subject: fix a unboundlocalerror occuring with python3 --- sphinx/ext/doctest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx/ext/doctest.py b/sphinx/ext/doctest.py index 9d681f90..62fbfdff 100644 --- a/sphinx/ext/doctest.py +++ b/sphinx/ext/doctest.py @@ -149,14 +149,14 @@ class TestCode(object): class SphinxDocTestRunner(doctest.DocTestRunner): def summarize(self, out, verbose=None): - io = StringIO.StringIO() + string_io = StringIO.StringIO() old_stdout = sys.stdout - sys.stdout = io + sys.stdout = string_io try: res = doctest.DocTestRunner.summarize(self, verbose) finally: sys.stdout = old_stdout - out(io.getvalue()) + out(string_io.getvalue()) return res def _DocTestRunner__patched_linecache_getlines(self, filename, -- cgit v1.2.1 From 83b1a06e03483a9fc476c18301a732e560b76cb8 Mon Sep 17 00:00:00 2001 From: DasIch Date: Tue, 15 Jun 2010 23:16:33 +0200 Subject: update distribute_setup.py from http://python-distribute.org/distribute_setup.py --- distribute_setup.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/distribute_setup.py b/distribute_setup.py index 4f7bd08c..37117b34 100644 --- a/distribute_setup.py +++ b/distribute_setup.py @@ -46,7 +46,7 @@ except ImportError: args = [quote(arg) for arg in args] return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 -DEFAULT_VERSION = "0.6.12" +DEFAULT_VERSION = "0.6.13" DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" SETUPTOOLS_FAKED_VERSION = "0.6c11" @@ -227,7 +227,6 @@ def _no_sandbox(function): return __no_sandbox -@_no_sandbox def _patch_file(path, content): """Will backup the file then patch it""" existing_content = open(path).read() @@ -244,6 +243,7 @@ def _patch_file(path, content): f.close() return True +_patch_file = _no_sandbox(_patch_file) def _same_content(path, content): return open(path).read() == content @@ -254,7 +254,6 @@ def _rename_path(path): os.rename(path, new_name) return new_name -@_no_sandbox def _remove_flat_installation(placeholder): if not os.path.isdir(placeholder): log.warn('Unkown installation at %s', placeholder) @@ -288,13 +287,13 @@ def _remove_flat_installation(placeholder): 'Setuptools distribution', element) return True +_remove_flat_installation = _no_sandbox(_remove_flat_installation) def _after_install(dist): log.warn('After install bootstrap.') placeholder = dist.get_command_obj('install').install_purelib _create_fake_setuptools_pkg_info(placeholder) -@_no_sandbox def _create_fake_setuptools_pkg_info(placeholder): if not placeholder or not os.path.exists(placeholder): log.warn('Could not find the install location') @@ -322,7 +321,8 @@ def _create_fake_setuptools_pkg_info(placeholder): finally: f.close() -@_no_sandbox +_create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info) + def _patch_egg_dir(path): # let's check if it's already patched pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') @@ -341,6 +341,7 @@ def _patch_egg_dir(path): f.close() return True +_patch_egg_dir = _no_sandbox(_patch_egg_dir) def _before_install(): log.warn('Before install bootstrap.') @@ -360,8 +361,8 @@ def _under_prefix(location): if len(args) > index: top_dir = args[index+1] return location.startswith(top_dir) - elif option == '--user' and USER_SITE is not None: - return location.startswith(USER_SITE) + if arg == '--user' and USER_SITE is not None: + return location.startswith(USER_SITE) return True @@ -420,6 +421,9 @@ def _fake_setuptools(): def _relaunch(): log.warn('Relaunching...') # we have to relaunch the process + # pip marker to avoid a relaunch bug + if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']: + sys.argv[0] = 'setup.py' args = [sys.executable] + sys.argv sys.exit(subprocess.call(args)) -- cgit v1.2.1 From 698d8d9e5b99605b9a0d1b27b4d8b9e7c658c08e Mon Sep 17 00:00:00 2001 From: DasIch Date: Wed, 16 Jun 2010 23:20:30 +0200 Subject: Encode strings after they have been formatted --- sphinx/builders/html.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index c5f7312a..4e07acf7 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -727,10 +727,12 @@ class StandaloneHTMLBuilder(Builder): self.info(bold('dumping object inventory... '), nonl=True) f = open(path.join(self.outdir, INVENTORY_FILENAME), 'wb') try: - f.write('# Sphinx inventory version 2\n') - f.write('# Project: %s\n' % self.config.project.encode('utf-8')) - f.write('# Version: %s\n' % self.config.version.encode('utf-8')) - f.write('# The remainder of this file is compressed using zlib.\n') + f.write((u'# Sphinx inventory version 2\n' + u'# Project: %s\n' + u'# Version: %s\n' + u'# The remainder of this file is compressed using zlib.\n' + % (self.config.project, self.config.version))\ + .encode('utf-8')) compressor = zlib.compressobj(9) for domainname, domain in self.env.domains.iteritems(): for name, dispname, type, docname, anchor, prio in \ @@ -742,11 +744,9 @@ class StandaloneHTMLBuilder(Builder): if dispname == name: dispname = u'-' f.write(compressor.compress( - '%s %s:%s %s %s %s\n' % (name.encode('utf-8'), - domainname.encode('utf-8'), - type.encode('utf-8'), prio, - uri.encode('utf-8'), - dispname.encode('utf-8')))) + (u'%s %s:%s %s %s %s\n' % (name, domainname, type, prio, + uri, dispname))\ + .encode('utf-8'))) f.write(compressor.flush()) finally: f.close() -- cgit v1.2.1 From 1db6898d76a8e7b567b60061eb089a96196b5951 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 18:58:18 +0200 Subject: Fix code formatting. --- sphinx/builders/html.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index 4e07acf7..0b39d38e 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -731,8 +731,8 @@ class StandaloneHTMLBuilder(Builder): u'# Project: %s\n' u'# Version: %s\n' u'# The remainder of this file is compressed using zlib.\n' - % (self.config.project, self.config.version))\ - .encode('utf-8')) + % (self.config.project, self.config.version) + ).encode('utf-8')) compressor = zlib.compressobj(9) for domainname, domain in self.env.domains.iteritems(): for name, dispname, type, docname, anchor, prio in \ @@ -744,9 +744,9 @@ class StandaloneHTMLBuilder(Builder): if dispname == name: dispname = u'-' f.write(compressor.compress( - (u'%s %s:%s %s %s %s\n' % (name, domainname, type, prio, - uri, dispname))\ - .encode('utf-8'))) + (u'%s %s:%s %s %s %s\n' % (name, domainname, type, + prio, uri, dispname) + ).encode('utf-8'))) f.write(compressor.flush()) finally: f.close() -- cgit v1.2.1 From 3b0bc3f3f1d1e196d18c5bc27a9a4e2516bf64d5 Mon Sep 17 00:00:00 2001 From: DasIch Date: Thu, 17 Jun 2010 05:26:19 +0200 Subject: Decode templates using utf-8 as jinja2 requires that for python3. This change will very likely cause problems for some people --- sphinx/util/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index 2ef420ed..ec48009f 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -18,6 +18,7 @@ import tempfile import posixpath import traceback from os import path +from codecs import open import docutils from docutils.utils import relative_path @@ -140,8 +141,8 @@ def copy_static_entry(source, targetdir, builder, context={}, target = path.join(targetdir, path.basename(source)) if source.lower().endswith('_t') and builder.templates: # templated! - fsrc = open(source, 'rb') - fdst = open(target[:-2], 'wb') + fsrc = open(source, 'r', encoding='utf-8') + fdst = open(target[:-2], 'w', encoding='utf-8') fdst.write(builder.templates.render_string(fsrc.read(), context)) fsrc.close() fdst.close() -- cgit v1.2.1 From 8801c9b53e160772ceeeca95326679fa7bc676c4 Mon Sep 17 00:00:00 2001 From: DasIch Date: Thu, 17 Jun 2010 05:38:22 +0200 Subject: Removed duplicated code --- sphinx/builders/qthelp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/builders/qthelp.py b/sphinx/builders/qthelp.py index ffc52334..53c7a9b1 100644 --- a/sphinx/builders/qthelp.py +++ b/sphinx/builders/qthelp.py @@ -230,7 +230,7 @@ class QtHelpBuilder(StandaloneHTMLBuilder): link = node['refuri'] title = escape(node.astext()).replace('"','"') item = section_template % {'title': title, 'ref': link} - item = ' '*4*indentlevel + item.encode('ascii', 'xmlcharrefreplace') + item = u' ' * 4 * indentlevel + item parts.append(item.encode('ascii', 'xmlcharrefreplace')) elif isinstance(node, nodes.bullet_list): for subnode in node: -- cgit v1.2.1 From 8381cbfed12fed44179d084f24fb7b900b5dee23 Mon Sep 17 00:00:00 2001 From: DasIch Date: Thu, 17 Jun 2010 05:47:44 +0200 Subject: Fixed error caused by mixing of string types --- sphinx/builders/qthelp.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sphinx/builders/qthelp.py b/sphinx/builders/qthelp.py index 53c7a9b1..e86f1921 100644 --- a/sphinx/builders/qthelp.py +++ b/sphinx/builders/qthelp.py @@ -130,8 +130,16 @@ class QtHelpBuilder(StandaloneHTMLBuilder): for indexname, indexcls, content, collapse in self.domain_indices: item = section_template % {'title': indexcls.localname, 'ref': '%s.html' % indexname} - sections.append(' '*4*4 + item) - sections = '\n'.join(sections) + sections.append((' ' * 4 * 4 + item).encode('utf-8')) + # sections may be unicode strings or byte strings, we have to make sure + # they are all byte strings before joining them + new_sections = [] + for section in sections: + if isinstance(section, unicode): + new_sections.append(section.encode('utf-8')) + else: + new_sections.append(section) + sections = u'\n'.encode('utf-8').join(new_sections) # keywords keywords = [] -- cgit v1.2.1 From ddf6657c7dd68f6a5d561e8d153235559506f85c Mon Sep 17 00:00:00 2001 From: DasIch Date: Thu, 17 Jun 2010 05:51:24 +0200 Subject: Don't mix string types when writing to a stream --- sphinx/builders/htmlhelp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sphinx/builders/htmlhelp.py b/sphinx/builders/htmlhelp.py index 538f4c84..e3a58e72 100644 --- a/sphinx/builders/htmlhelp.py +++ b/sphinx/builders/htmlhelp.py @@ -258,7 +258,8 @@ class HTMLHelpBuilder(StandaloneHTMLBuilder): def write_index(title, refs, subitems): def write_param(name, value): item = ' \n' % (name, value) - f.write(item.encode('ascii', 'xmlcharrefreplace')) + f.write(item.encode('ascii', 'xmlcharrefreplace') + .decode('ascii')) title = cgi.escape(title) f.write('
  • \n') write_param('Keyword', title) -- cgit v1.2.1 From fd90e518d403b10ba503f406050a1fe6c0f3986a Mon Sep 17 00:00:00 2001 From: DasIch Date: Thu, 17 Jun 2010 05:56:06 +0200 Subject: the sphinx.search.js_index is now able to load and dump data from binary streams --- sphinx/search.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sphinx/search.py b/sphinx/search.py index 729b63b2..6d5a0614 100644 --- a/sphinx/search.py +++ b/sphinx/search.py @@ -58,10 +58,13 @@ class _JavaScriptIndex(object): return jsdump.loads(data) def dump(self, data, f): - f.write(self.dumps(data)) + f.write(self.dumps(data).encode('utf-8')) def load(self, f): - return self.loads(f.read()) + data = f.read() + if isinstance(data, unicode): + return self.loads(data) + return self.loads(data.decode('utf-8')) js_index = _JavaScriptIndex() -- cgit v1.2.1 From 0d055b80a97daaa29c49a2b0e507d60fa7a0e9a3 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sat, 19 Jun 2010 16:38:52 +0200 Subject: Fix test to respect the new .truncate() behavior --- tests/test_application.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_application.py b/tests/test_application.py index 3d287a57..d1154863 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -45,9 +45,11 @@ def test_output(): app = TestApp(status=status, warning=warnings) try: status.truncate(0) # __init__ writes to status + status.seek(0) app.info("Nothing here...") assert status.getvalue() == "Nothing here...\n" status.truncate(0) + status.seek(0) app.info("Nothing here...", True) assert status.getvalue() == "Nothing here..." -- cgit v1.2.1 From 64b37e03e42f94ddd9486d0afd1ffd5e47cc1f4e Mon Sep 17 00:00:00 2001 From: DasIch Date: Sat, 19 Jun 2010 21:50:00 +0200 Subject: make doctest work with python2 and python3 --- tests/root/conf.py | 2 +- tests/root/doctest.txt | 38 +++++++++++++++++++------------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/root/conf.py b/tests/root/conf.py index 2b6d6a9a..b4734189 100644 --- a/tests/root/conf.py +++ b/tests/root/conf.py @@ -22,7 +22,7 @@ copyright = '2010, Georg Brandl & Team' version = '0.6' release = '0.6alpha1' today_fmt = '%B %d, %Y' -#unused_docs = [] +# unused_docs = [] exclude_patterns = ['_build', '**/excluded.*'] keep_warnings = True pygments_style = 'sphinx' diff --git a/tests/root/doctest.txt b/tests/root/doctest.txt index 35cdd589..6ac0b286 100644 --- a/tests/root/doctest.txt +++ b/tests/root/doctest.txt @@ -30,7 +30,7 @@ Special directives .. testcode:: - print 1+1 + print(1+1) .. testoutput:: @@ -50,30 +50,30 @@ Special directives .. testsetup:: * - from math import floor + from math import factorial .. doctest:: - >>> floor(1.2) - 1.0 + >>> factorial(1) + 1 .. testcode:: - print floor(1.2) + print(factorial(1)) .. testoutput:: - 1.0 + 1 - >>> floor(1.2) - 1.0 + >>> factorial(1) + 1 * options for testcode/testoutput blocks .. testcode:: :hide: - print 'Output text.' + print('Output text.') .. testoutput:: :hide: @@ -85,36 +85,36 @@ Special directives .. testsetup:: group1 - from math import ceil + from math import trunc - ``ceil`` is now known in "group1", but not in others. + ``trunc`` is now known in "group1", but not in others. .. doctest:: group1 - >>> ceil(0.8) - 1.0 + >>> trunc(1.1) + 1 .. doctest:: group2 - >>> ceil(0.8) + >>> trunc(1.1) Traceback (most recent call last): ... - NameError: name 'ceil' is not defined + NameError: name 'trunc' is not defined Interleaving testcode/testoutput: .. testcode:: group1 - print ceil(0.8) + print(factorial(3)) .. testcode:: group2 - print floor(0.8) + print(factorial(4)) .. testoutput:: group1 - 1.0 + 6 .. testoutput:: group2 - 0.0 + 24 -- cgit v1.2.1 From 6680b807c17db68681a98a89f92a3dec944d2eda Mon Sep 17 00:00:00 2001 From: DasIch Date: Sat, 19 Jun 2010 19:58:28 +0200 Subject: Fixed the coverage extension test as well as the coverage extension itself for python3 --- sphinx/ext/coverage.py | 5 ++++- tests/path.py | 27 +++++++++++++++++++++++++++ tests/test_coverage.py | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py index 4924d30b..f41820e2 100644 --- a/sphinx/ext/coverage.py +++ b/sphinx/ext/coverage.py @@ -173,8 +173,11 @@ class CoverageBuilder(Builder): attrs = [] + for attr_name in dir(obj): + attr = getattr(obj, attr_name) for attr_name, attr in inspect.getmembers( - obj, inspect.ismethod): + obj, lambda x: inspect.ismethod(x) or \ + inspect.isfunction(x)): if attr_name[0] == '_': # starts with an underscore, ignore it continue diff --git a/tests/path.py b/tests/path.py index 28a8c22a..df96bce4 100644 --- a/tests/path.py +++ b/tests/path.py @@ -142,6 +142,33 @@ class path(str): finally: f.close() + def bytes(self): + """ + Returns the bytes in the file. + """ + f = open(self, mode='rb') + try: + return f.read() + finally: + f.close() + + def write_bytes(self, bytes, append=False): + """ + Writes the given `bytes` to the file. + + :param append: + If ``True`` given `bytes` are added at the end of the file. + """ + if append: + mode = 'ab' + else: + mode = 'wb' + f = open(self, mode=mode) + try: + f.write(bytes) + finally: + f.close() + def exists(self): """ Returns ``True`` if the path exist. diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 1262ebf5..cb831635 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -33,7 +33,7 @@ def test_build(app): assert 'api.h' in c_undoc assert ' * Py_SphinxTest' in c_undoc - undoc_py, undoc_c = pickle.loads((app.outdir / 'undoc.pickle').text()) + undoc_py, undoc_c = pickle.loads((app.outdir / 'undoc.pickle').bytes()) assert len(undoc_c) == 1 # the key is the full path to the header file, which isn't testable assert undoc_c.values()[0] == [('function', 'Py_SphinxTest')] -- cgit v1.2.1 From af21ba45bd7bfb4e37b17d92304ed87bed2368cf Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 20 Jun 2010 18:50:22 +0200 Subject: make sure to encode strings passed to md5 --- sphinx/builders/html.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index 0b39d38e..5a3e9bb3 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -146,8 +146,9 @@ class StandaloneHTMLBuilder(Builder): cfgdict = dict((name, self.config[name]) for (name, desc) in self.config.values.iteritems() if desc[1] == 'html') - self.config_hash = md5(str(cfgdict)).hexdigest() - self.tags_hash = md5(str(sorted(self.tags))).hexdigest() + self.config_hash = md5(unicode(cfgdict).encode('ascii')).hexdigest() + self.tags_hash = md5(unicode(sorted(self.tags)).encode('ascii')) \ + .hexdigest() old_config_hash = old_tags_hash = '' try: fp = open(path.join(self.outdir, '.buildinfo')) -- cgit v1.2.1 From cba79ccd558f32500e0a039492c126280b91d4ed Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 20 Jun 2010 18:54:19 +0200 Subject: Use utf-8 instead of ascii to encode strings for hashing --- sphinx/builders/html.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index 5a3e9bb3..16afd347 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -146,8 +146,8 @@ class StandaloneHTMLBuilder(Builder): cfgdict = dict((name, self.config[name]) for (name, desc) in self.config.values.iteritems() if desc[1] == 'html') - self.config_hash = md5(unicode(cfgdict).encode('ascii')).hexdigest() - self.tags_hash = md5(unicode(sorted(self.tags)).encode('ascii')) \ + self.config_hash = md5(unicode(cfgdict).encode('utf-8')).hexdigest() + self.tags_hash = md5(unicode(sorted(self.tags)).encode('utf-8')) \ .hexdigest() old_config_hash = old_tags_hash = '' try: -- cgit v1.2.1 From 2f961d167d0c8a3c26a62a1771032c232cc5cf65 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 20 Jun 2010 19:34:49 +0200 Subject: Fix warning for bytestrings with non-ascii content for python3 --- sphinx/config.py | 12 +++++++++--- tests/test_config.py | 9 +++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/sphinx/config.py b/sphinx/config.py index 07c3d63a..7ec5cfe8 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -11,12 +11,18 @@ import os import re +import sys from os import path from sphinx.errors import ConfigError from sphinx.util.osutil import make_filename -nonascii_re = re.compile(r'[\x80-\xff]') +nonascii_re = re.compile(ur'[\x80-\xff]'.encode('ascii')) + +try: + bytes +except NameError: + bytes = str class Config(object): @@ -187,10 +193,10 @@ class Config(object): # check all string values for non-ASCII characters in bytestrings, # since that can result in UnicodeErrors all over the place for name, value in self._raw_config.iteritems(): - if isinstance(value, str) and nonascii_re.search(value): + if isinstance(value, bytes) and nonascii_re.search(value): warn('the config value %r is set to a string with non-ASCII ' 'characters; this can lead to Unicode errors occurring. ' - 'Please use Unicode strings, e.g. u"Content".' % name) + 'Please use Unicode strings, e.g. %r.' % (name, u'Content')) def init_values(self): config = self._raw_config diff --git a/tests/test_config.py b/tests/test_config.py index 23d92e39..ecf90f60 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -9,6 +9,7 @@ :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ +import sys from util import * @@ -88,8 +89,12 @@ def test_errors_warnings(dir): raises_msg(ConfigError, 'conf.py', Config, dir, 'conf.py', {}, None) # test the warning for bytestrings with non-ascii content - write_file(dir / 'conf.py', - u'# -*- coding: latin-1\nproject = "fooä"\n', 'latin-1') + # bytestrings with non-ascii content are a syntax error in python3 so we + # skip the test there + if sys.version_info >= (3, 0): + return + write_file(dir / 'conf.py', u'# -*- coding: latin-1\nproject = "fooä"\n', + 'latin-1') cfg = Config(dir, 'conf.py', {}, None) warned = [False] def warn(msg): -- cgit v1.2.1 From a0b222468f87591dd088a07b357b68c915c87067 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 19:13:25 +0200 Subject: Move bytes to pycompat. --- sphinx/config.py | 8 ++------ sphinx/util/pycompat.py | 5 +++++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/sphinx/config.py b/sphinx/config.py index 7ec5cfe8..210bb9e2 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -16,13 +16,9 @@ from os import path from sphinx.errors import ConfigError from sphinx.util.osutil import make_filename +from sphinx.util.pycompat import bytes, b -nonascii_re = re.compile(ur'[\x80-\xff]'.encode('ascii')) - -try: - bytes -except NameError: - bytes = str +nonascii_re = re.compile(b(r'[\x80-\xff]')) class Config(object): diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index 0725545d..624749fc 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -41,6 +41,11 @@ except NameError: def next(iterator): return iterator.next() +try: + bytes = bytes +except NameError: + bytes = str + try: any = any -- cgit v1.2.1 From 5cf185dcabe9a0fbe3c5f164fa59bbbd67d1862a Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 20 Jun 2010 22:24:00 +0200 Subject: fix line length --- sphinx/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sphinx/config.py b/sphinx/config.py index 210bb9e2..273bb97d 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -192,7 +192,8 @@ class Config(object): if isinstance(value, bytes) and nonascii_re.search(value): warn('the config value %r is set to a string with non-ASCII ' 'characters; this can lead to Unicode errors occurring. ' - 'Please use Unicode strings, e.g. %r.' % (name, u'Content')) + 'Please use Unicode strings, e.g. %r.' % (name, u'Content') + ) def init_values(self): config = self._raw_config -- cgit v1.2.1 From db6d04b5fcf265b0ce0e3f3afa7807ed5de91399 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 20 Jun 2010 22:39:38 +0200 Subject: Fixed warnings in python3 --- sphinx/environment.py | 2 +- tests/test_build_html.py | 5 +++++ tests/test_build_latex.py | 3 +++ tests/util.py | 7 ++++++- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/sphinx/environment.py b/sphinx/environment.py index 21994a74..fd171d44 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -80,7 +80,7 @@ class WarningStream(object): self.warnfunc = warnfunc def write(self, text): if text.strip(): - self.warnfunc(text, None, '') + self.warnfunc(text.strip(), None, '') class NoUri(Exception): diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 4dee513a..65c1840e 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -12,6 +12,7 @@ import os import re import htmlentitydefs +import sys from StringIO import StringIO try: @@ -60,6 +61,10 @@ def tail_check(check): return checker +if sys.version_info >= (3, 0): + ENV_WARNINGS = remove_unicode_literals(ENV_WARNINGS) + HTML_WARNINGS = remove_unicode_literals(HTML_WARNINGS) + HTML_XPATH = { 'images.html': [ (".//img[@src='_images/img.png']", ''), diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index 4405395a..6c1ccad9 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -32,6 +32,9 @@ None:None: WARNING: no matching candidate for image URI u'foo.\\*' WARNING: invalid pair index entry u'' """ +if sys.version_info >= (3, 0): + LATEX_WARNINGS = remove_unicode_literals(LATEX_WARNINGS) + @with_app(buildername='latex', warning=latex_warnfile, cleanenv=True) def test_latex(app): diff --git a/tests/util.py b/tests/util.py index 2cf4a775..950ea23f 100644 --- a/tests/util.py +++ b/tests/util.py @@ -11,6 +11,7 @@ import sys import StringIO import tempfile import shutil +import re from codecs import open try: @@ -32,7 +33,7 @@ __all__ = [ 'raises', 'raises_msg', 'Struct', 'ListOutput', 'TestApp', 'with_app', 'gen_with_app', 'path', 'with_tempdir', 'write_file', - 'sprint', + 'sprint', 'remove_unicode_literals', ] @@ -206,3 +207,7 @@ def write_file(name, contents, encoding=None): def sprint(*args): sys.stderr.write(' '.join(map(str, args)) + '\n') + +_unicode_literals_re = re.compile(r'u(".*")|u(\'.*\')') +def remove_unicode_literals(s): + return _unicode_literals_re.sub(lambda x: x.group(1) or x.group(2), s) -- cgit v1.2.1 From 68e145cf0816da9301217240cbba4be9b43a9589 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 19:15:04 +0200 Subject: Make string contents nongreedy. --- tests/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/util.py b/tests/util.py index 950ea23f..b81e15b6 100644 --- a/tests/util.py +++ b/tests/util.py @@ -208,6 +208,6 @@ def write_file(name, contents, encoding=None): def sprint(*args): sys.stderr.write(' '.join(map(str, args)) + '\n') -_unicode_literals_re = re.compile(r'u(".*")|u(\'.*\')') +_unicode_literals_re = re.compile(r'u(".*?")|u(\'.*?\')') def remove_unicode_literals(s): return _unicode_literals_re.sub(lambda x: x.group(1) or x.group(2), s) -- cgit v1.2.1 From b658c351d3479df5d54f88eff3f10166c7253a81 Mon Sep 17 00:00:00 2001 From: DasIch Date: Tue, 22 Jun 2010 22:49:58 +0200 Subject: the test suite now runs on ubuntu, hopefully also debian and other system --- tests/run.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/run.py b/tests/run.py index f384fe26..59a3ffa5 100755 --- a/tests/run.py +++ b/tests/run.py @@ -11,13 +11,14 @@ """ import sys -from os import path, chdir +from os import path, chdir, listdir if sys.version_info >= (3,): print('Copying and converting sources to build/lib/tests...') from distutils.util import copydir_run_2to3 testroot = path.dirname(__file__) or '.' - newroot = path.join(testroot, path.pardir, 'build', 'lib', 'tests') + newroot = path.join(testroot, path.pardir, 'build') + newroot = path.join(newroot, listdir(newroot)[0], 'tests') copydir_run_2to3(testroot, newroot) # switch to the converted dir so nose tests the right tests chdir(newroot) -- cgit v1.2.1 From 5cc8cecdec5eb4181cffa1ca3b5c4081472970dd Mon Sep 17 00:00:00 2001 From: DasIch Date: Sat, 10 Jul 2010 19:47:32 +0200 Subject: Removed XMLParser._fixtext which fixes several errors in the test suite --- tests/etree13/ElementTree.py | 17 ++++------------- tests/test_build_html.py | 2 +- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/tests/etree13/ElementTree.py b/tests/etree13/ElementTree.py index d3732504..e50ad640 100644 --- a/tests/etree13/ElementTree.py +++ b/tests/etree13/ElementTree.py @@ -1425,13 +1425,6 @@ class XMLParser(object): err.position = value.lineno, value.offset raise err - def _fixtext(self, text): - # convert text string to ascii, if possible - try: - return text.encode("ascii") - except UnicodeError: - return text - def _fixname(self, key): # expand qname, and convert name string to ascii, if possible try: @@ -1440,30 +1433,28 @@ class XMLParser(object): name = key if "}" in name: name = "{" + name - self._names[key] = name = self._fixtext(name) + self._names[key] return name def _start(self, tag, attrib_in): fixname = self._fixname - fixtext = self._fixtext tag = fixname(tag) attrib = {} for key, value in attrib_in.items(): - attrib[fixname(key)] = fixtext(value) + attrib[fixname(key)] = value return self.target.start(tag, attrib) def _start_list(self, tag, attrib_in): fixname = self._fixname - fixtext = self._fixtext tag = fixname(tag) attrib = {} if attrib_in: for i in range(0, len(attrib_in), 2): - attrib[fixname(attrib_in[i])] = fixtext(attrib_in[i+1]) + attrib[fixname(attrib_in[i])] = attrib_in[i+1] return self.target.start(tag, attrib) def _data(self, text): - return self.target.data(self._fixtext(text)) + return self.target.data(text) def _end(self, tag): return self.target.end(self._fixname(tag)) diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 65c1840e..5e3a2018 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -258,7 +258,7 @@ class NslessParser(ET.XMLParser): br = name.find('}') if br > 0: name = name[br+1:] - self._names[key] = name = self._fixtext(name) + self._names[key] = name return name -- cgit v1.2.1 From 69de288fb75fc12d161a8441be4fe1fd4477f775 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 11 Jul 2010 00:57:08 +0200 Subject: Revert changes from the last commit which caused problems with 2.x --- tests/etree13/ElementTree.py | 17 +++++++++++++---- tests/test_build_html.py | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/etree13/ElementTree.py b/tests/etree13/ElementTree.py index e50ad640..d3732504 100644 --- a/tests/etree13/ElementTree.py +++ b/tests/etree13/ElementTree.py @@ -1425,6 +1425,13 @@ class XMLParser(object): err.position = value.lineno, value.offset raise err + def _fixtext(self, text): + # convert text string to ascii, if possible + try: + return text.encode("ascii") + except UnicodeError: + return text + def _fixname(self, key): # expand qname, and convert name string to ascii, if possible try: @@ -1433,28 +1440,30 @@ class XMLParser(object): name = key if "}" in name: name = "{" + name - self._names[key] + self._names[key] = name = self._fixtext(name) return name def _start(self, tag, attrib_in): fixname = self._fixname + fixtext = self._fixtext tag = fixname(tag) attrib = {} for key, value in attrib_in.items(): - attrib[fixname(key)] = value + attrib[fixname(key)] = fixtext(value) return self.target.start(tag, attrib) def _start_list(self, tag, attrib_in): fixname = self._fixname + fixtext = self._fixtext tag = fixname(tag) attrib = {} if attrib_in: for i in range(0, len(attrib_in), 2): - attrib[fixname(attrib_in[i])] = attrib_in[i+1] + attrib[fixname(attrib_in[i])] = fixtext(attrib_in[i+1]) return self.target.start(tag, attrib) def _data(self, text): - return self.target.data(text) + return self.target.data(self._fixtext(text)) def _end(self, tag): return self.target.end(self._fixname(tag)) diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 5e3a2018..65c1840e 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -258,7 +258,7 @@ class NslessParser(ET.XMLParser): br = name.find('}') if br > 0: name = name[br+1:] - self._names[key] = name + self._names[key] = name = self._fixtext(name) return name -- cgit v1.2.1 From 8355164b22844c6f234513631a298cde046211af Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 11 Jul 2010 01:05:27 +0200 Subject: Provided a working fix for the remaining errors in the test suite --- tests/etree13/ElementTree.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/etree13/ElementTree.py b/tests/etree13/ElementTree.py index d3732504..f459c7f8 100644 --- a/tests/etree13/ElementTree.py +++ b/tests/etree13/ElementTree.py @@ -1425,12 +1425,16 @@ class XMLParser(object): err.position = value.lineno, value.offset raise err - def _fixtext(self, text): - # convert text string to ascii, if possible - try: - return text.encode("ascii") - except UnicodeError: + if sys.version_info >= (3, 0): + def _fixtext(self, text): return text + else: + def _fixtext(self, text): + # convert text string to ascii, if possible + try: + return text.encode("ascii") + except UnicodeError: + return text def _fixname(self, key): # expand qname, and convert name string to ascii, if possible -- cgit v1.2.1 From c3de719cc5f7ed256ad3bfe39925c313b9cc99d1 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 11 Jul 2010 11:32:16 +0200 Subject: Fixed test_env.test_images test for python3 --- tests/test_env.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_env.py b/tests/test_env.py index 4ecbaac4..124ed08c 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -8,6 +8,7 @@ :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ +import sys from util import * @@ -54,8 +55,10 @@ def test_images(): app._warning.reset() htmlbuilder = StandaloneHTMLBuilder(app) htmlbuilder.post_process_images(tree) - assert "no matching candidate for image URI u'foo.*'" in \ - app._warning.content[-1] + image_uri_message = "no matching candidate for image URI u'foo.*'" + if sys.version_info >= (3, 0): + image_uri_message = remove_unicode_literals(image_uri_message) + assert image_uri_message in app._warning.content[-1] assert set(htmlbuilder.images.keys()) == \ set(['subdir/img.png', 'img.png', 'subdir/simg.png', 'svgimg.svg']) assert set(htmlbuilder.images.values()) == \ @@ -64,8 +67,7 @@ def test_images(): app._warning.reset() latexbuilder = LaTeXBuilder(app) latexbuilder.post_process_images(tree) - assert "no matching candidate for image URI u'foo.*'" in \ - app._warning.content[-1] + assert image_uri_message in app._warning.content[-1] assert set(latexbuilder.images.keys()) == \ set(['subdir/img.png', 'subdir/simg.png', 'img.png', 'img.pdf', 'svgimg.pdf']) -- cgit v1.2.1 From ff445040232d83f025c8d8842804234d348f5ee3 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 11 Jul 2010 12:24:50 +0200 Subject: Don't use (in this case) unnecessary python2 unicode literals --- tests/root/literal.inc | 2 +- tests/test_build_html.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/root/literal.inc b/tests/root/literal.inc index d5b9890c..694f15ed 100644 --- a/tests/root/literal.inc +++ b/tests/root/literal.inc @@ -1,7 +1,7 @@ # Literally included file using Python highlighting # -*- coding: utf-8 -*- -foo = u"Including Unicode characters: üöä" +foo = "Including Unicode characters: üöä" class Foo: pass diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 65c1840e..3ca2c757 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -232,7 +232,7 @@ if pygments: (".//div[@class='inc-lines highlight-text']//pre", r'^class Foo:\n pass\nclass Bar:\n$'), (".//div[@class='inc-startend highlight-text']//pre", - ur'^foo = u"Including Unicode characters: üöä"\n$'), + ur'^foo = "Including Unicode characters: üöä"\n$'), (".//div[@class='inc-preappend highlight-text']//pre", r'(?m)^START CODE$'), (".//div[@class='inc-pyobj-dedent highlight-python']//span", -- cgit v1.2.1 From 7e90901a9352685d50c64c5fefb8423bc9906672 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sat, 24 Jul 2010 13:04:30 +0200 Subject: Added Python 3.x support to the changelog again under a different section (1.1) --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index b247543f..573b4edd 100644 --- a/CHANGES +++ b/CHANGES @@ -23,6 +23,8 @@ Release 1.0.1 (Jul 27, 2010) * Fix hyperrefs in object descriptions for LaTeX. + * Added Python 3.x support. + Release 1.0 (Jul 23, 2010) ========================== -- cgit v1.2.1 From 76711ef8e03812f399321035db2caa8d903ebcb1 Mon Sep 17 00:00:00 2001 From: DasIch Date: Tue, 27 Jul 2010 21:01:27 +0200 Subject: Fixed the JSONHTMLBuilder --- sphinx/builders/html.py | 49 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index 16afd347..064f9d2f 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -754,6 +754,10 @@ class StandaloneHTMLBuilder(Builder): self.info('done') def dump_search_index(self): + # NOTE: If you change this code you have to change it in + # JSONHTMLBuilder.dump_search_index as well because the code is + # mostly copied from here for reasons explained in a comment in + # said method. self.info(bold('dumping search index... '), nonl=True) self.indexer.prune(self.env.all_docs) searchindexfn = path.join(self.outdir, self.searchindex_filename) @@ -938,6 +942,13 @@ class SerializingHTMLBuilder(StandaloneHTMLBuilder): return docname[:-5] # up to sep return docname + SEP + def dump_context(self, context, filename): + f = open(filename, 'wb') + try: + self.implementation.dump(context, f, 2) + finally: + f.close() + def handle_page(self, pagename, ctx, templatename='page.html', outfilename=None, event_arg=None): ctx['current_page_name'] = pagename @@ -951,11 +962,7 @@ class SerializingHTMLBuilder(StandaloneHTMLBuilder): ctx, event_arg) ensuredir(path.dirname(outfilename)) - f = open(outfilename, 'wb') - try: - self.implementation.dump(ctx, f, 2) - finally: - f.close() + self.dump_context(ctx, outfilename) # if there is a source file, copy the source file for the # "show source" link @@ -968,11 +975,7 @@ class SerializingHTMLBuilder(StandaloneHTMLBuilder): def handle_finish(self): # dump the global context outfilename = path.join(self.outdir, self.globalcontext_filename) - f = open(outfilename, 'wb') - try: - self.implementation.dump(self.globalcontext, f, 2) - finally: - f.close() + self.dump_context(self.globalcontext, outfilename) # super here to dump the search index StandaloneHTMLBuilder.handle_finish(self) @@ -1019,3 +1022,29 @@ class JSONHTMLBuilder(SerializingHTMLBuilder): 'The module simplejson (or json in Python >= 2.6) ' 'is not available. The JSONHTMLBuilder builder will not work.') SerializingHTMLBuilder.init(self) + + def dump_context(self, context, filename): + # json operates entirely on "unicode" but the filesystem doesn't so we + # have to specify an encoding. + f = codecs.open(filename, 'w', encoding='utf-8') + try: + self.implementation.dump(context, f, 2) + finally: + f.close() + + def dump_search_index(self): + # this code is nearly completely copied from the super class, in which + # this method was initially defined, the only difference is that we + # specify an encoding for the file. + self.info(bold('dumping search index...'), nonl=True) + self.indexer.prune(self.env.all_docs) + searchindexfn = path.join(self.outdir, self.searchindex_filename) + # first write to a temporary file, so that if dumping fails, + # the existing index won't be overwritten + f = codecs.open(searchindexfn + '.tmp', 'w', encoding='utf-8') + try: + self.indexer.dump(f, self.indexer_format) + finally: + f.close() + movefile(searchindexfn + '.tmp', searchindexfn) + self.info('done') -- cgit v1.2.1 From 6b95a4a689b9e44fea3b43a8df4b8edf3b5eef1d Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 19:24:48 +0200 Subject: Declare if serializers/indexers dump unicode or bytes. Removes duplication of methods. --- sphinx/builders/html.py | 47 +++++++++++++++-------------------------------- sphinx/search.py | 7 ++----- 2 files changed, 17 insertions(+), 37 deletions(-) diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index 064f9d2f..5a7d49cd 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -63,6 +63,7 @@ class StandaloneHTMLBuilder(Builder): out_suffix = '.html' link_suffix = '.html' # defaults to matching out_suffix indexer_format = js_index + indexer_dumps_unicode = True supported_image_types = ['image/svg+xml', 'image/png', 'image/gif', 'image/jpeg'] searchindex_filename = 'searchindex.js' @@ -754,16 +755,15 @@ class StandaloneHTMLBuilder(Builder): self.info('done') def dump_search_index(self): - # NOTE: If you change this code you have to change it in - # JSONHTMLBuilder.dump_search_index as well because the code is - # mostly copied from here for reasons explained in a comment in - # said method. self.info(bold('dumping search index... '), nonl=True) self.indexer.prune(self.env.all_docs) searchindexfn = path.join(self.outdir, self.searchindex_filename) # first write to a temporary file, so that if dumping fails, # the existing index won't be overwritten - f = open(searchindexfn + '.tmp', 'wb') + if self.indexer_dumps_unicode: + f = codecs.open(searchindexfn + '.tmp', 'w', encoding='utf-8') + else: + f = open(searchindexfn + '.tmp', 'wb') try: self.indexer.dump(f, self.indexer_format) finally: @@ -920,6 +920,7 @@ class SerializingHTMLBuilder(StandaloneHTMLBuilder): #: implements a `dump`, `load`, `dumps` and `loads` functions #: (pickle, simplejson etc.) implementation = None + implementation_dumps_unicode = False #: the filename for the global context file globalcontext_filename = None @@ -943,8 +944,12 @@ class SerializingHTMLBuilder(StandaloneHTMLBuilder): return docname + SEP def dump_context(self, context, filename): - f = open(filename, 'wb') + if self.implementation_dumps_unicode: + f = codecs.open(filename, 'w', encoding='utf-8') + else: + f = open(filename, 'wb') try: + # XXX: the third argument is pickle-specific! self.implementation.dump(context, f, 2) finally: f.close() @@ -995,7 +1000,9 @@ class PickleHTMLBuilder(SerializingHTMLBuilder): A Builder that dumps the generated HTML into pickle files. """ implementation = pickle + implementation_dumps_unicode = False indexer_format = pickle + indexer_dumps_unicode = False name = 'pickle' out_suffix = '.fpickle' globalcontext_filename = 'globalcontext.pickle' @@ -1010,7 +1017,9 @@ class JSONHTMLBuilder(SerializingHTMLBuilder): A builder that dumps the generated HTML into JSON files. """ implementation = jsonimpl + implementation_dumps_unicode = True indexer_format = jsonimpl + indexer_dumps_unicode = True name = 'json' out_suffix = '.fjson' globalcontext_filename = 'globalcontext.json' @@ -1022,29 +1031,3 @@ class JSONHTMLBuilder(SerializingHTMLBuilder): 'The module simplejson (or json in Python >= 2.6) ' 'is not available. The JSONHTMLBuilder builder will not work.') SerializingHTMLBuilder.init(self) - - def dump_context(self, context, filename): - # json operates entirely on "unicode" but the filesystem doesn't so we - # have to specify an encoding. - f = codecs.open(filename, 'w', encoding='utf-8') - try: - self.implementation.dump(context, f, 2) - finally: - f.close() - - def dump_search_index(self): - # this code is nearly completely copied from the super class, in which - # this method was initially defined, the only difference is that we - # specify an encoding for the file. - self.info(bold('dumping search index...'), nonl=True) - self.indexer.prune(self.env.all_docs) - searchindexfn = path.join(self.outdir, self.searchindex_filename) - # first write to a temporary file, so that if dumping fails, - # the existing index won't be overwritten - f = codecs.open(searchindexfn + '.tmp', 'w', encoding='utf-8') - try: - self.indexer.dump(f, self.indexer_format) - finally: - f.close() - movefile(searchindexfn + '.tmp', searchindexfn) - self.info('done') diff --git a/sphinx/search.py b/sphinx/search.py index 6d5a0614..729b63b2 100644 --- a/sphinx/search.py +++ b/sphinx/search.py @@ -58,13 +58,10 @@ class _JavaScriptIndex(object): return jsdump.loads(data) def dump(self, data, f): - f.write(self.dumps(data).encode('utf-8')) + f.write(self.dumps(data)) def load(self, f): - data = f.read() - if isinstance(data, unicode): - return self.loads(data) - return self.loads(data.decode('utf-8')) + return self.loads(f.read()) js_index = _JavaScriptIndex() -- cgit v1.2.1 From f714133ad1807ae95c33a8ba294b288d4ca3af0a Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 19:27:45 +0200 Subject: Give a binary document name. --- tests/test_search.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_search.py b/tests/test_search.py index 0b5b158b..c0750366 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -13,6 +13,7 @@ from docutils import frontend, utils from docutils.parsers import rst from sphinx.search import IndexBuilder +from sphinx.util.pycompat import b settings = parser = None @@ -31,7 +32,7 @@ test that non-comments are indexed: fermion ''' def test_wordcollector(): - doc = utils.new_document('test data', settings) + doc = utils.new_document(b('test data'), settings) doc['file'] = 'dummy' parser.parse(FILE_CONTENTS, doc) -- cgit v1.2.1 From ab96b364732568ec02fc2dd150ace1728df0895d Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 16 May 2010 17:51:40 +0200 Subject: Stop modifying PYTHONPATH in the Makefile --- Makefile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Makefile b/Makefile index 21a87e36..13228c78 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,5 @@ PYTHON ?= python3 -export PYTHONPATH = $(shell echo "$$PYTHONPATH"):./sphinx - .PHONY: all check clean clean-pyc clean-patchfiles clean-generated pylint \ reindent test -- cgit v1.2.1 From 7fb4bbb7f884107ee022ba2c2df2dd82c7718a8d Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 19:31:05 +0200 Subject: Factor out a replace(). --- sphinx/environment.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sphinx/environment.py b/sphinx/environment.py index fd171d44..70690a8a 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -383,14 +383,14 @@ class BuildEnvironment: If base is a path string, return absolute path under that. If suffix is not None, add it instead of config.source_suffix. """ + docname = docname.replace(SEP, path.sep) suffix = suffix or self.config.source_suffix if base is True: - return path.join(self.srcdir, - docname.replace(SEP, path.sep)) + suffix + return path.join(self.srcdir, docname) + suffix elif base is None: - return docname.replace(SEP, path.sep) + suffix + return docname + suffix else: - return path.join(base, docname.replace(SEP, path.sep)) + suffix + return path.join(base, docname) + suffix def find_files(self, config): """ -- cgit v1.2.1 From 269b1b355da74fe15c8d1a9ccb02b8e7d65c9a7b Mon Sep 17 00:00:00 2001 From: DasIch Date: Thu, 3 Jun 2010 17:33:50 +0200 Subject: test if decoding is required first --- sphinx/ext/autodoc.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sphinx/ext/autodoc.py b/sphinx/ext/autodoc.py index 1113f97a..efb762e2 100644 --- a/sphinx/ext/autodoc.py +++ b/sphinx/ext/autodoc.py @@ -435,8 +435,11 @@ class Documenter(object): # set sourcename and add content from attribute documentation if self.analyzer: # prevent encoding errors when the file name is non-ASCII - filename = unicode(self.analyzer.srcname, - sys.getfilesystemencoding(), 'replace') + if not isinstance(self.analyzer.srcname, unicode): + filename = unicode(self.analyzer.srcname, + sys.getfilesystemencoding(), 'replace') + else: + filename = self.analyzer.srcname sourcename = u'%s:docstring of %s' % (filename, self.fullname) attr_docs = self.analyzer.find_attr_docs() -- cgit v1.2.1 From ba12de17cb7743941207f311efb52c1b9c85a6c3 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sun, 6 Jun 2010 23:57:37 +0200 Subject: pass paths as bytes to docutils --- sphinx/environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/environment.py b/sphinx/environment.py index 70690a8a..46ec6016 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -649,7 +649,7 @@ class BuildEnvironment: destination_class=NullOutput) pub.set_components(None, 'restructuredtext', None) pub.process_programmatic_settings(None, self.settings, None) - pub.set_source(None, src_path) + pub.set_source(None, src_path.encode(FILESYSTEMENCODING)) pub.set_destination(None, None) try: pub.publish() -- cgit v1.2.1 From f479aeb8fbb7bc9eb305e3a01113288ce0df38ce Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 19:33:24 +0200 Subject: Introduce constant. --- sphinx/environment.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sphinx/environment.py b/sphinx/environment.py index 46ec6016..6ad28ec8 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -11,6 +11,7 @@ import re import os +import sys import time import types import codecs @@ -43,6 +44,7 @@ from sphinx.util.pycompat import all, class_types from sphinx.errors import SphinxError, ExtensionError from sphinx.locale import _ +fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() orig_role_function = roles.role orig_directive_function = directives.directive @@ -649,7 +651,7 @@ class BuildEnvironment: destination_class=NullOutput) pub.set_components(None, 'restructuredtext', None) pub.process_programmatic_settings(None, self.settings, None) - pub.set_source(None, src_path.encode(FILESYSTEMENCODING)) + pub.set_source(None, src_path.encode(fs_encoding)) pub.set_destination(None, None) try: pub.publish() -- cgit v1.2.1 From 1a1a47de5041da79cb1e5b11ee87e384775dea35 Mon Sep 17 00:00:00 2001 From: DasIch Date: Mon, 7 Jun 2010 00:07:38 +0200 Subject: only decode code if necessary --- sphinx/ext/viewcode.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 81881beb..db04ac79 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -31,7 +31,11 @@ def doctree_read(app, doctree): env._viewcode_modules[modname] = False return analyzer.find_tags() - entry = analyzer.code.decode(analyzer.encoding), analyzer.tags, {} + if not isinstance(analyzer.code, unicode): + code = analyzer.code.decode(analyzer.encoding) + else: + code = analyzer.code + entry = code, analyzer.tags, {} env._viewcode_modules[modname] = entry elif entry is False: return -- cgit v1.2.1 From c70d3921c4f0ace54bf198e44babc357011713ba Mon Sep 17 00:00:00 2001 From: DasIch Date: Wed, 16 Jun 2010 23:05:56 +0200 Subject: only decode if possible --- sphinx/environment.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sphinx/environment.py b/sphinx/environment.py index 6ad28ec8..bf255165 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -630,6 +630,8 @@ class BuildEnvironment: class SphinxSourceClass(FileInput): def decode(self_, data): + if isinstance(data, unicode): + return data return data.decode(self_.encoding, 'sphinx') def read(self_): -- cgit v1.2.1 From 73e10b85af46fc7588d865db3ed4ba3a083be163 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 19:35:35 +0200 Subject: Mode "Ub" does not exist. --- sphinx/config.py | 2 +- sphinx/directives/code.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/config.py b/sphinx/config.py index 273bb97d..708da162 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -167,7 +167,7 @@ class Config(object): try: try: os.chdir(dirname) - f = open(config_file, 'Ub') + f = open(config_file, 'rb') try: code = compile(f.read(), config_file, 'exec') finally: diff --git a/sphinx/directives/code.py b/sphinx/directives/code.py index 54d8edf1..1808cdab 100644 --- a/sphinx/directives/code.py +++ b/sphinx/directives/code.py @@ -119,7 +119,7 @@ class LiteralInclude(Directive): encoding = self.options.get('encoding', env.config.source_encoding) codec_info = codecs.lookup(encoding) try: - f = codecs.StreamReaderWriter(open(fn, 'Ub'), + f = codecs.StreamReaderWriter(open(fn, 'rb'), codec_info[2], codec_info[3], 'strict') lines = f.readlines() f.close() -- cgit v1.2.1 From 41e44bc87e76642cf4e2bc32f63431dbcf24bfb4 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 28 Jul 2010 19:36:57 +0200 Subject: Unify version_info checks. --- sphinx/util/pycompat.py | 2 +- tests/run.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index 624749fc..229b54b4 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -27,7 +27,7 @@ except NameError: # the ubiquitous "bytes" helper function -if sys.version_info > (3, 0): +if sys.version_info >= (3, 0): def b(s): return s.encode('utf-8') else: diff --git a/tests/run.py b/tests/run.py index 59a3ffa5..50567fbc 100755 --- a/tests/run.py +++ b/tests/run.py @@ -13,7 +13,7 @@ import sys from os import path, chdir, listdir -if sys.version_info >= (3,): +if sys.version_info >= (3, 0): print('Copying and converting sources to build/lib/tests...') from distutils.util import copydir_run_2to3 testroot = path.dirname(__file__) or '.' -- cgit v1.2.1 From 3d2a8e538735f8462cd796b89fdafdbdd41f7660 Mon Sep 17 00:00:00 2001 From: DasIch Date: Sat, 19 Jun 2010 18:23:49 +0200 Subject: Implemented sphinx.ext.autodoc.MethodDocumenter.import_object for python3 --- sphinx/ext/autodoc.py | 57 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/sphinx/ext/autodoc.py b/sphinx/ext/autodoc.py index efb762e2..eef181de 100644 --- a/sphinx/ext/autodoc.py +++ b/sphinx/ext/autodoc.py @@ -251,6 +251,9 @@ class Documenter(object): self.retann = None # the object to document (set after import_object succeeds) self.object = None + self.object_name = None + # the parent/owner of the object to document + self.parent = None # the module analyzer to get at attribute docs, or None self.analyzer = None @@ -316,9 +319,13 @@ class Documenter(object): """ try: __import__(self.modname) + parent = None obj = self.module = sys.modules[self.modname] for part in self.objpath: + parent = obj obj = self.get_attr(obj, part) + self.object_name = part + self.parent = parent self.object = obj return True # this used to only catch SyntaxError, ImportError and AttributeError, @@ -1007,24 +1014,38 @@ class MethodDocumenter(ClassLevelDocumenter): return inspect.isroutine(member) and \ not isinstance(parent, ModuleDocumenter) - def import_object(self): - ret = ClassLevelDocumenter.import_object(self) - if isinstance(self.object, classmethod) or \ - (isinstance(self.object, MethodType) and - self.object.im_self is not None): - self.directivetype = 'classmethod' - # document class and static members before ordinary ones - self.member_order = self.member_order - 1 - elif isinstance(self.object, FunctionType) or \ - (isinstance(self.object, BuiltinFunctionType) and - hasattr(self.object, '__self__') and - self.object.__self__ is not None): - self.directivetype = 'staticmethod' - # document class and static members before ordinary ones - self.member_order = self.member_order - 1 - else: - self.directivetype = 'method' - return ret + if sys.version_info >= (3, 0): + def import_object(self): + ret = ClassLevelDocumenter.import_object(self) + obj_from_parent = self.parent.__dict__.get(self.object_name) + if isinstance(obj_from_parent, classmethod): + self.directivetype = 'classmethod' + self.member_order = self.member_order - 1 + elif isinstance(obj_from_parent, staticmethod): + self.directivetype = 'staticmethod' + self.member_order = self.member_order - 1 + else: + self.directivetype = 'method' + return ret + else: + def import_object(self): + ret = ClassLevelDocumenter.import_object(self) + if isinstance(self.object, classmethod) or \ + (isinstance(self.object, MethodType) and + self.object.im_self is not None): + self.directivetype = 'classmethod' + # document class and static members before ordinary ones + self.member_order = self.member_order - 1 + elif isinstance(self.object, FunctionType) or \ + (isinstance(self.object, BuiltinFunctionType) and + hasattr(self.object, '__self__') and + self.object.__self__ is not None): + self.directivetype = 'staticmethod' + # document class and static members before ordinary ones + self.member_order = self.member_order - 1 + else: + self.directivetype = 'method' + return ret def format_args(self): if inspect.isbuiltin(self.object) or \ -- cgit v1.2.1 From e9b4aec500672dc8d7018c6f20610f8a7a6607a0 Mon Sep 17 00:00:00 2001 From: DasIch Date: Fri, 28 May 2010 04:08:15 +0200 Subject: Fix SyntaxError in config generated by quickstart and the quickstart test --- sphinx/quickstart.py | 14 +++++++++++++- tests/test_quickstart.py | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/sphinx/quickstart.py b/sphinx/quickstart.py index 892bd641..557f8c09 100644 --- a/sphinx/quickstart.py +++ b/sphinx/quickstart.py @@ -9,7 +9,7 @@ :license: BSD, see LICENSE for details. """ -import sys, os, time +import sys, os, time, re from os import path from codecs import open @@ -685,6 +685,18 @@ def do_prompt(d, key, text, default=None, validator=nonempty): d[key] = x +if sys.version_info >= (3, 0): + # remove Unicode literal prefixes + _unicode_string_re = re.compile(r"[uU]('.*?')") + def _convert_python_source(source): + return _unicode_string_re.sub('\\1', source) + + for f in ['QUICKSTART_CONF', 'EPUB_CONFIG', 'INTERSPHINX_CONFIG']: + globals()[f] = convert_python_source(globals()[f]) + + del _unicode_string_re, _convert_python_source + + def inner_main(args): d = {} texescape.init() diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index d0403d3b..72ae764d 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -117,8 +117,8 @@ def test_quickstart_all_answers(tempdir): 'Root path': tempdir, 'Separate source and build': 'y', 'Name prefix for templates': '.', - 'Project name': 'STASI\xe2\x84\xa2', - 'Author name': 'Wolfgang Sch\xc3\xa4uble & G\'Beckstein', + 'Project name': u'STASI™'.encode('utf-8'), + 'Author name': u'Wolfgang Schäuble & G\'Beckstein'.encode('utf-8'), 'Project version': '2.0', 'Project release': '2.0.1', 'Source file suffix': '.txt', -- cgit v1.2.1 From ba8e7e4c17069a92145d114d695d10c979f4d479 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Sat, 29 May 2010 18:14:42 +0200 Subject: Skeleton for PO builder. --- sphinx/builders/intl.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 sphinx/builders/intl.py diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py new file mode 100644 index 00000000..5bc18697 --- /dev/null +++ b/sphinx/builders/intl.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +""" + sphinx.builders.intl + ~~~~~~~~~~~~~~~~~~~~ + + The MessageCatalogBuilder class. + + :copyright: Copyright 2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from sphinx.builders import Builder + +class MessageCatalogBuilder(Builder): + pass + + def get_target_uri(self, docname, typ=None): + return '' + + def get_outdated_docs(self): + return self.env.found_docs + + def prepare_writing(self, docnames): + return + + def write_doc(self, docname, doctree): + return + + def finish(self): + return -- cgit v1.2.1 From a590748de6c555fa14b59b31967591f81769b912 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Sat, 29 May 2010 20:26:38 +0200 Subject: Add PO builder to sphinx-quickstart. --- sphinx/quickstart.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/sphinx/quickstart.py b/sphinx/quickstart.py index 8479575a..0aab261e 100644 --- a/sphinx/quickstart.py +++ b/sphinx/quickstart.py @@ -321,7 +321,7 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) \ $(SPHINXOPTS) %(rsrcdir)s .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp \ -epub latex latexpdf text man changes linkcheck doctest +epub latex latexpdf text man changes linkcheck doctest gettext help: \t@echo "Please use \\`make ' where is one of" @@ -338,6 +338,7 @@ help: \t@echo " latexpdf to make LaTeX files and run them through pdflatex" \t@echo " text to make text files" \t@echo " man to make manual pages" +\t@echo " gettext to make PO message catalogs" \t@echo " changes to make an overview of all changed/added/deprecated items" \t@echo " linkcheck to check all external links for integrity" \t@echo " doctest to run all doctests embedded in the documentation \ @@ -424,6 +425,11 @@ man: \t@echo \t@echo "Build finished. The manual pages are in $(BUILDDIR)/man." +gettext: +\t$(SPHINXBUILD) -b gettext $(ALLSPHINXOPTS) $(BUILDDIR)/locale +\t@echo +\t@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + changes: \t$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes \t@echo @@ -472,6 +478,7 @@ if "%%1" == "help" ( \techo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter \techo. text to make text files \techo. man to make manual pages +\techo. gettext to make PO message catalogs \techo. changes to make an overview over all changed/added/deprecated items \techo. linkcheck to check all external links for integrity \techo. doctest to run all doctests embedded in the documentation if enabled @@ -573,6 +580,13 @@ if "%%1" == "man" ( \tgoto end ) +if "%%1" == "gettext" ( +\t%%SPHINXBUILD%% -b gettext %%ALLSPHINXOPTS%% %%BUILDDIR%%/locale +\techo. +\techo.Build finished. The message catalogs are in %%BUILDDIR%%/locale. +\tgoto end +) + if "%%1" == "changes" ( \t%%SPHINXBUILD%% -b changes %%ALLSPHINXOPTS%% %%BUILDDIR%%/changes \techo. -- cgit v1.2.1 From c565e1f4e754eeaf8410918bf5cb787186455de6 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Sun, 30 May 2010 09:36:23 +0200 Subject: Add gettext build using intl.MessageCatalogBuilder. --- sphinx/builders/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index e345d570..cf7f3955 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -329,4 +329,5 @@ BUILTIN_BUILDERS = { 'man': ('manpage', 'ManualPageBuilder'), 'changes': ('changes', 'ChangesBuilder'), 'linkcheck': ('linkcheck', 'CheckExternalLinksBuilder'), + 'gettext': ('intl', 'MessageCatalogBuilder'), } -- cgit v1.2.1 From 5484b963a8de4d4f6951ca2ef5cf1a4486fd8ed4 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Sun, 30 May 2010 17:02:54 +0200 Subject: Add gettext builder to Sphinx docs. --- doc/Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/Makefile b/doc/Makefile index b873c7e5..ff3cb22b 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -29,6 +29,7 @@ help: @echo " epub to make an epub file" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run pdflatex" + @echo " gettext to make PO message catalogs" @echo " changes to make an overview over all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @@ -112,6 +113,11 @@ latexpdf: make -C _build/latex all-pdf @echo "pdflatex finished; the PDF files are in _build/latex." +gettext: + $(SPHINXBUILD) -b gettext $(ALLSPHINXOPTS) _build/locale + @echo + @echo "Build finished. The message catalogs are in _build/locale." + changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes @echo -- cgit v1.2.1 From 602b64c977de703ff0e9d1f41c3515fc40c1d546 Mon Sep 17 00:00:00 2001 From: jacob Date: Sun, 30 May 2010 20:52:24 -0500 Subject: Revert incomplete change in last revision. --- sphinx/cmdline.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sphinx/cmdline.py b/sphinx/cmdline.py index 8f8e6b31..e3e94465 100644 --- a/sphinx/cmdline.py +++ b/sphinx/cmdline.py @@ -21,7 +21,7 @@ from sphinx import __version__ from sphinx.errors import SphinxError from sphinx.application import Sphinx from sphinx.util import Tee, format_exception_cut_frames, save_traceback -from sphinx.util.console import red, nocolor, init_color +from sphinx.util.console import red, nocolor, color_terminal def usage(argv, msg=None): @@ -57,7 +57,10 @@ Modi: def main(argv): - init_color() + if not color_terminal(): + # Windows' poor cmd box doesn't understand ANSI sequences + nocolor() + try: opts, args = getopt.getopt(argv[1:], 'ab:t:d:c:CD:A:ng:NEqQWw:P') allopts = set(opt[0] for opt in opts) -- cgit v1.2.1 From 380f08b39f381e098a361a15523d5a13c93a3387 Mon Sep 17 00:00:00 2001 From: jacob Date: Sun, 30 May 2010 21:33:04 -0500 Subject: "Initial commit": Added sphinx.websupport module, as well as a builder and writer for the web support package. --- CHANGES.jacobmason | 2 ++ sphinx/builders/__init__.py | 1 + sphinx/builders/websupport.py | 56 +++++++++++++++++++++++++++++++++++++++++++ sphinx/websupport/__init__.py | 14 +++++++++++ sphinx/websupport/api.py | 40 +++++++++++++++++++++++++++++++ sphinx/websupport/document.py | 37 ++++++++++++++++++++++++++++ sphinx/writers/websupport.py | 29 ++++++++++++++++++++++ 7 files changed, 179 insertions(+) create mode 100644 CHANGES.jacobmason create mode 100644 sphinx/builders/websupport.py create mode 100644 sphinx/websupport/__init__.py create mode 100644 sphinx/websupport/api.py create mode 100644 sphinx/websupport/document.py create mode 100644 sphinx/writers/websupport.py diff --git a/CHANGES.jacobmason b/CHANGES.jacobmason new file mode 100644 index 00000000..42adc427 --- /dev/null +++ b/CHANGES.jacobmason @@ -0,0 +1,2 @@ +May 30: Added files builders/websupport.py, writers/websupport.py, +websupport/api.py, and websupport/document.api. Provides a rudimentary method of building websupport data, and rendering it as html. \ No newline at end of file diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index e345d570..328b2668 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -329,4 +329,5 @@ BUILTIN_BUILDERS = { 'man': ('manpage', 'ManualPageBuilder'), 'changes': ('changes', 'ChangesBuilder'), 'linkcheck': ('linkcheck', 'CheckExternalLinksBuilder'), + 'websupport': ('websupport', 'WebSupportBuilder'), } diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py new file mode 100644 index 00000000..d39bbd6d --- /dev/null +++ b/sphinx/builders/websupport.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +""" + sphinx.builders.websupport + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Builder for the web support package. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from os import path + +from sphinx.util.osutil import ensuredir, os_path +from sphinx.builders.html import PickleHTMLBuilder +from sphinx.writers.websupport import WebSupportTranslator + +class WebSupportBuilder(PickleHTMLBuilder): + """ + Builds documents for the web support package. + """ + name = 'websupport' + template_suffix = '.html' + + def init_translator_class(self): + self.translator_class = WebSupportTranslator + + def write_doc(self, docname, doctree): + # The translator needs the docuname to generate ids. + self.docname = docname + PickleHTMLBuilder.write_doc(self, docname, doctree) + + def handle_page(self, pagename, ctx, templatename='', **ignored): + # Mostly copied from PickleHTMLBuilder. + ctx['current_page_name'] = pagename + self.add_sidebars(pagename, ctx) + + self.app.emit('html-page-context', pagename, ctx) + + # Instead of pickling ctx as PickleHTMLBuilder does, we + # create a Document object and pickle that. + document = self.docwriter.visitor.support_document + document.body = ctx['body'] if 'body' in ctx else '' + document.title = ctx['title'] if 'title' in ctx else '' + + doc_filename = path.join(self.outdir, + os_path(pagename) + self.out_suffix) + ensuredir(path.dirname(doc_filename)) + f = open(doc_filename, 'wb') + try: + self.implementation.dump(document, f, 2) + finally: + f.close() + + def get_target_uri(self, docname, typ=None): + return docname diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py new file mode 100644 index 00000000..36c2dcc9 --- /dev/null +++ b/sphinx/websupport/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" + sphinx.websupport + ~~~~~~~~~~~~~~~~~ + + Web Support Package + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from sphinx.websupport.api import WebSupport + +support = WebSupport() diff --git a/sphinx/websupport/api.py b/sphinx/websupport/api.py new file mode 100644 index 00000000..da6fc9e1 --- /dev/null +++ b/sphinx/websupport/api.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +""" + sphinx.websupport.api + ~~~~~~~~~~~~~~~~~~~~ + + All API functions. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import cPickle as pickle +from os import path + +from jinja2 import Template + +from sphinx.application import Sphinx + +class WebSupport(object): + + def init(self, srcdir, outdir='', comment_html=''): + self.srcdir = srcdir + self.outdir = outdir or os.path.join(self.srcdir, '_build', + 'websupport') + self.comment_template = Template(comment_html) + + def build(self, **kwargs): + doctreedir = kwargs.pop('doctreedir', + path.join(self.outdir, 'doctrees')) + app = Sphinx(self.srcdir, self.srcdir, + self.outdir, doctreedir, 'websupport') + app.build() + + def get_document(self, docname): + infilename = path.join(self.outdir, docname + '.fpickle') + f = open(infilename, 'rb') + document = pickle.load(f) + # The document renders the comment_template. + document.comment_template = self.comment_template + return document diff --git a/sphinx/websupport/document.py b/sphinx/websupport/document.py new file mode 100644 index 00000000..d1f5677b --- /dev/null +++ b/sphinx/websupport/document.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +""" + sphinx.websupport.document + ~~~~~~~~~~~~~~~~~~~~ + + Contains a Document class for working with Sphinx documents. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from os import path + +from jinja2 import Template +from docutils import nodes +from sphinx import addnodes + +class Document(object): + """A single Document such as 'index'.""" + def __init__(self): + self.commentable_nodes = [] + self.template = None + + def add_commentable(self, node_id, rst_source=''): + node = CommentableNode(node_id, rst_source) + + def render_comment(self, id): + return self.comment_template.render(id=id) + + def render_html(self, comments=False): + template = Template(self.body) + return template.render(render_comment=self.render_comment) + +class CommentableNode(object): + def __init__(self, id, rst_source=''): + self.id = id + self.rst_source='' diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py new file mode 100644 index 00000000..3b250755 --- /dev/null +++ b/sphinx/writers/websupport.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +""" + sphinx.writers.websupport + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + docutils writers handling Sphinx' custom nodes. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from sphinx.writers.html import HTMLTranslator +from sphinx.websupport.document import Document + +class WebSupportTranslator(HTMLTranslator): + """ + Our custom HTML translator. + """ + def __init__(self, builder, *args, **kwargs): + HTMLTranslator.__init__(self, builder, *args, **kwargs) + self.support_document = Document() + self.current_id = 0 + + def depart_paragraph(self, node): + HTMLTranslator.depart_paragraph(self, node) + self.support_document.add_commentable(self.current_id) + self.body.append("{{ render_comment('%s-p%s') }}" % + (self.builder.docname, self.current_id)) + self.current_id += 1 -- cgit v1.2.1 From 346b5b05417fc37890f3bbb081bc600c57c3d07b Mon Sep 17 00:00:00 2001 From: jacob Date: Sun, 30 May 2010 22:20:59 -0500 Subject: Fixed bad call to os.path --- sphinx/websupport/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/websupport/api.py b/sphinx/websupport/api.py index da6fc9e1..3cf112a3 100644 --- a/sphinx/websupport/api.py +++ b/sphinx/websupport/api.py @@ -20,8 +20,8 @@ class WebSupport(object): def init(self, srcdir, outdir='', comment_html=''): self.srcdir = srcdir - self.outdir = outdir or os.path.join(self.srcdir, '_build', - 'websupport') + self.outdir = outdir or path.join(self.srcdir, '_build', + 'websupport') self.comment_template = Template(comment_html) def build(self, **kwargs): -- cgit v1.2.1 From cea33842a4f2d636261cf4f5ce89c14d33a7b06d Mon Sep 17 00:00:00 2001 From: jacob Date: Mon, 31 May 2010 16:07:47 -0500 Subject: Switched to creating a list of html slices --- sphinx/builders/websupport.py | 6 ++---- sphinx/websupport/api.py | 7 +------ sphinx/websupport/document.py | 22 ++++++++-------------- sphinx/writers/websupport.py | 23 +++++++++++++++++++---- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index d39bbd6d..f6b64849 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -26,7 +26,7 @@ class WebSupportBuilder(PickleHTMLBuilder): self.translator_class = WebSupportTranslator def write_doc(self, docname, doctree): - # The translator needs the docuname to generate ids. + # The translator needs the docname to generate ids. self.docname = docname PickleHTMLBuilder.write_doc(self, docname, doctree) @@ -38,10 +38,8 @@ class WebSupportBuilder(PickleHTMLBuilder): self.app.emit('html-page-context', pagename, ctx) # Instead of pickling ctx as PickleHTMLBuilder does, we - # create a Document object and pickle that. + # have created a Document object and pickle that. document = self.docwriter.visitor.support_document - document.body = ctx['body'] if 'body' in ctx else '' - document.title = ctx['title'] if 'title' in ctx else '' doc_filename = path.join(self.outdir, os_path(pagename) + self.out_suffix) diff --git a/sphinx/websupport/api.py b/sphinx/websupport/api.py index 3cf112a3..eca24fb5 100644 --- a/sphinx/websupport/api.py +++ b/sphinx/websupport/api.py @@ -12,17 +12,14 @@ import cPickle as pickle from os import path -from jinja2 import Template - from sphinx.application import Sphinx class WebSupport(object): - def init(self, srcdir, outdir='', comment_html=''): + def init(self, srcdir, outdir=''): self.srcdir = srcdir self.outdir = outdir or path.join(self.srcdir, '_build', 'websupport') - self.comment_template = Template(comment_html) def build(self, **kwargs): doctreedir = kwargs.pop('doctreedir', @@ -35,6 +32,4 @@ class WebSupport(object): infilename = path.join(self.outdir, docname + '.fpickle') f = open(infilename, 'rb') document = pickle.load(f) - # The document renders the comment_template. - document.comment_template = self.comment_template return document diff --git a/sphinx/websupport/document.py b/sphinx/websupport/document.py index d1f5677b..16a60934 100644 --- a/sphinx/websupport/document.py +++ b/sphinx/websupport/document.py @@ -18,20 +18,14 @@ from sphinx import addnodes class Document(object): """A single Document such as 'index'.""" def __init__(self): - self.commentable_nodes = [] - self.template = None + self.slices = [] - def add_commentable(self, node_id, rst_source=''): - node = CommentableNode(node_id, rst_source) + def add_slice(self, html, id=None, commentable=False): + slice = HTMLSlice(html, id, commentable) + self.slices.append(slice) - def render_comment(self, id): - return self.comment_template.render(id=id) - - def render_html(self, comments=False): - template = Template(self.body) - return template.render(render_comment=self.render_comment) - -class CommentableNode(object): - def __init__(self, id, rst_source=''): +class HTMLSlice(object): + def __init__(self, html, id, commentable): + self.html = html self.id = id - self.rst_source='' + self.commentable = commentable diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index 3b250755..e712b133 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -18,12 +18,27 @@ class WebSupportTranslator(HTMLTranslator): """ def __init__(self, builder, *args, **kwargs): HTMLTranslator.__init__(self, builder, *args, **kwargs) + self.init_support() + + def init_support(self): self.support_document = Document() self.current_id = 0 + + def handle_visit_commentable(self, node): + self.support_document.add_slice(''.join(self.body)) + self.body = [] + + def handle_depart_commentable(self, node): + slice_id = '%s-%s' % (self.builder.docname, self.current_id) + self.support_document.add_slice(''.join(self.body), + slice_id, commentable=True) + self.body = [] + self.current_id += 1 + + def visit_paragraph(self, node): + HTMLTranslator.visit_paragraph(self, node) + self.handle_visit_commentable(node) def depart_paragraph(self, node): HTMLTranslator.depart_paragraph(self, node) - self.support_document.add_commentable(self.current_id) - self.body.append("{{ render_comment('%s-p%s') }}" % - (self.builder.docname, self.current_id)) - self.current_id += 1 + self.handle_depart_commentable(node) -- cgit v1.2.1 From d419d213ea20563d025d9b8da943cb4c483f967b Mon Sep 17 00:00:00 2001 From: jacob Date: Mon, 31 May 2010 17:12:32 -0500 Subject: Slice at bullet_list, desc, or paragraph now. --- sphinx/writers/websupport.py | 47 ++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index e712b133..a1e59788 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -16,29 +16,46 @@ class WebSupportTranslator(HTMLTranslator): """ Our custom HTML translator. """ + commentable_nodes = ['bullet_list', 'paragraph', 'desc'] + def __init__(self, builder, *args, **kwargs): HTMLTranslator.__init__(self, builder, *args, **kwargs) self.init_support() def init_support(self): self.support_document = Document() + self.in_commentable = False self.current_id = 0 + def dispatch_visit(self, node): + if node.__class__.__name__ in self.commentable_nodes: + self.handle_visit_commentable(node) + HTMLTranslator.dispatch_visit(self, node) + + def dispatch_departure(self, node): + HTMLTranslator.dispatch_departure(self, node) + if node.__class__.__name__ in self.commentable_nodes: + self.handle_depart_commentable(node) + def handle_visit_commentable(self, node): - self.support_document.add_slice(''.join(self.body)) - self.body = [] + # If we are already recording a commentable slice we don't do + # anything. We can't handle nesting. + if not self.in_commentable: + self.support_document.add_slice(''.join(self.body)) + node.commented = self.in_commentable = True + self.body = [] + else: + node.commented = False def handle_depart_commentable(self, node): - slice_id = '%s-%s' % (self.builder.docname, self.current_id) - self.support_document.add_slice(''.join(self.body), - slice_id, commentable=True) - self.body = [] - self.current_id += 1 - - def visit_paragraph(self, node): - HTMLTranslator.visit_paragraph(self, node) - self.handle_visit_commentable(node) - - def depart_paragraph(self, node): - HTMLTranslator.depart_paragraph(self, node) - self.handle_depart_commentable(node) + assert(self.in_commentable) + if node.commented: + slice_id = '%s-%s' % (self.builder.docname, self.current_id) + self.current_id += 1 + + body = ''.join(self.body) + self.support_document.add_slice(body, slice_id, commentable=True) + + self.in_commentable = False + self.body = [] + -- cgit v1.2.1 From 4fac85ed6e8783d1a0a78d0d0094434fa5a104ab Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Wed, 2 Jun 2010 09:17:42 +0200 Subject: Collect raw messages for translation. --- sphinx/builders/intl.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 5bc18697..0aecdcf3 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -9,10 +9,14 @@ :license: BSD, see LICENSE for details. """ +import collections +from docutils import nodes + from sphinx.builders import Builder class MessageCatalogBuilder(Builder): - pass + def init(self): + self.catalogs = collections.defaultdict(list) def get_target_uri(self, docname, typ=None): return '' @@ -24,7 +28,9 @@ class MessageCatalogBuilder(Builder): return def write_doc(self, docname, doctree): - return + catalog = self.catalogs[docname.split('/')[0]] + for node in doctree.traverse(nodes.TextElement): + catalog.append(node.astext()) def finish(self): return -- cgit v1.2.1 From 3d6a99d2ef277857b400ca98dbe9e439078d613d Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Wed, 2 Jun 2010 09:18:30 +0200 Subject: Normalize messages for later rewrapping. --- sphinx/builders/intl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 0aecdcf3..2cbb17cf 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -30,7 +30,8 @@ class MessageCatalogBuilder(Builder): def write_doc(self, docname, doctree): catalog = self.catalogs[docname.split('/')[0]] for node in doctree.traverse(nodes.TextElement): - catalog.append(node.astext()) + msg = node.astext().replace('\n', ' ') + catalog.append(msg) def finish(self): return -- cgit v1.2.1 From 9d8c20eb185f38fd2941204a48551913b5e9e0eb Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Wed, 2 Jun 2010 09:19:08 +0200 Subject: Ignore invisible and inline nodes during translation. --- sphinx/builders/intl.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 2cbb17cf..9373baae 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -30,6 +30,8 @@ class MessageCatalogBuilder(Builder): def write_doc(self, docname, doctree): catalog = self.catalogs[docname.split('/')[0]] for node in doctree.traverse(nodes.TextElement): + if isinstance(node, (nodes.Invisible, nodes.Inline)): + continue msg = node.astext().replace('\n', ' ') catalog.append(msg) -- cgit v1.2.1 From f15a3aa7534b52d5c7e4ba0dec5083a144705c69 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Wed, 2 Jun 2010 09:35:33 +0200 Subject: Write message catalogs to POT files. --- sphinx/builders/intl.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 9373baae..c40fff26 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -10,6 +10,8 @@ """ import collections +from os import path + from docutils import nodes from sphinx.builders import Builder @@ -36,4 +38,8 @@ class MessageCatalogBuilder(Builder): catalog.append(msg) def finish(self): - return + for section, messages in self.catalogs.iteritems(): + pofile = open(path.join(self.outdir, '%s.pot' % section), 'w') + for message in messages: + pomsg = u'msgid "%s"\nmsgstr ""\n\n' % message + pofile.write(pomsg.encode('utf-8')) -- cgit v1.2.1 From 1f1edb2d35490fe691b52488fb89e97cf6645661 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Wed, 2 Jun 2010 09:45:13 +0200 Subject: Add meta information to PO headers. --- sphinx/builders/intl.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index c40fff26..3224d92f 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -16,6 +16,27 @@ from docutils import nodes from sphinx.builders import Builder +POHEADER = r""" +# SOME DESCRIPTIVE TITLE. +# Copyright (C) %(copyright)s +# This file is distributed under the same license as the %(project)s package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-05-08 18:29+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +"""[1:] + class MessageCatalogBuilder(Builder): def init(self): self.catalogs = collections.defaultdict(list) @@ -40,6 +61,7 @@ class MessageCatalogBuilder(Builder): def finish(self): for section, messages in self.catalogs.iteritems(): pofile = open(path.join(self.outdir, '%s.pot' % section), 'w') + pofile.write(POHEADER % self.config) for message in messages: pomsg = u'msgid "%s"\nmsgstr ""\n\n' % message pofile.write(pomsg.encode('utf-8')) -- cgit v1.2.1 From 9ac37d800b08ee84b60b9ba00cce950ff274514e Mon Sep 17 00:00:00 2001 From: jacob Date: Wed, 2 Jun 2010 19:16:12 -0500 Subject: Fixed bug that clipped the end of bodies. --- sphinx/writers/websupport.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index a1e59788..6d255e64 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -59,3 +59,8 @@ class WebSupportTranslator(HTMLTranslator): self.in_commentable = False self.body = [] + def depart_document(self, node): + assert(not self.in_commentable) + self.support_document.add_slice(''.join(self.body)) + + -- cgit v1.2.1 From f6999aed0127894f17a4cb17edf60e1d7dc9b10e Mon Sep 17 00:00:00 2001 From: jacob Date: Wed, 2 Jun 2010 19:22:33 -0500 Subject: Added ctx to document. --- sphinx/builders/websupport.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index f6b64849..48e895db 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -32,7 +32,7 @@ class WebSupportBuilder(PickleHTMLBuilder): def handle_page(self, pagename, ctx, templatename='', **ignored): # Mostly copied from PickleHTMLBuilder. - ctx['current_page_name'] = pagename + ctx['current_page_name'] = ctx['pagename'] = pagename self.add_sidebars(pagename, ctx) self.app.emit('html-page-context', pagename, ctx) @@ -40,13 +40,14 @@ class WebSupportBuilder(PickleHTMLBuilder): # Instead of pickling ctx as PickleHTMLBuilder does, we # have created a Document object and pickle that. document = self.docwriter.visitor.support_document + document.__dict__.update(ctx) doc_filename = path.join(self.outdir, os_path(pagename) + self.out_suffix) ensuredir(path.dirname(doc_filename)) f = open(doc_filename, 'wb') try: - self.implementation.dump(document, f, 2) + self.implementation.dump(document, f) finally: f.close() -- cgit v1.2.1 From 14b4523abab773429acde67541da65cbaddb59a8 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Fri, 4 Jun 2010 16:54:37 +0200 Subject: Add public name to MessageCatalogBuilder. --- sphinx/builders/intl.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 3224d92f..8966cf8b 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -38,6 +38,8 @@ msgstr "" """[1:] class MessageCatalogBuilder(Builder): + name = 'gettext' + def init(self): self.catalogs = collections.defaultdict(list) -- cgit v1.2.1 From 819d52cd32295bb18fe55ee193e40e54ae226e0d Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Fri, 4 Jun 2010 16:55:35 +0200 Subject: Escaped quotation marks in msgids. --- sphinx/builders/intl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 8966cf8b..6291c405 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -65,5 +65,6 @@ class MessageCatalogBuilder(Builder): pofile = open(path.join(self.outdir, '%s.pot' % section), 'w') pofile.write(POHEADER % self.config) for message in messages: + message = message.replace(u'"', ur'\"') pomsg = u'msgid "%s"\nmsgstr ""\n\n' % message pofile.write(pomsg.encode('utf-8')) -- cgit v1.2.1 From 371f5166553993298188e14025b74c416358bfff Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Fri, 4 Jun 2010 17:14:04 +0200 Subject: Properly close open .pot files. --- sphinx/builders/intl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 6291c405..f388537d 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -68,3 +68,4 @@ class MessageCatalogBuilder(Builder): message = message.replace(u'"', ur'\"') pomsg = u'msgid "%s"\nmsgstr ""\n\n' % message pofile.write(pomsg.encode('utf-8')) + pofile.close() -- cgit v1.2.1 From 54adc35e7ef9e9565d0129ddf5195b4eb1656a41 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Fri, 4 Jun 2010 18:21:19 +0200 Subject: Use progress indicator for gettext builds. --- sphinx/builders/intl.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index f388537d..67937582 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -15,6 +15,7 @@ from os import path from docutils import nodes from sphinx.builders import Builder +from sphinx.util.console import darkgreen POHEADER = r""" # SOME DESCRIPTIVE TITLE. @@ -61,7 +62,9 @@ class MessageCatalogBuilder(Builder): catalog.append(msg) def finish(self): - for section, messages in self.catalogs.iteritems(): + for section, messages in self.status_iterator( + self.catalogs.iteritems(), "writing message catalogs... ", + lambda (section, _):darkgreen(section), len(self.catalogs)): pofile = open(path.join(self.outdir, '%s.pot' % section), 'w') pofile.write(POHEADER % self.config) for message in messages: -- cgit v1.2.1 From 272d06727c35191bc658993f313d8ab1efcaf122 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Fri, 4 Jun 2010 18:23:06 +0200 Subject: Safeguard file.close() against failure. --- sphinx/builders/intl.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 67937582..dc6cbf25 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -66,9 +66,11 @@ class MessageCatalogBuilder(Builder): self.catalogs.iteritems(), "writing message catalogs... ", lambda (section, _):darkgreen(section), len(self.catalogs)): pofile = open(path.join(self.outdir, '%s.pot' % section), 'w') - pofile.write(POHEADER % self.config) - for message in messages: - message = message.replace(u'"', ur'\"') - pomsg = u'msgid "%s"\nmsgstr ""\n\n' % message - pofile.write(pomsg.encode('utf-8')) - pofile.close() + try: + pofile.write(POHEADER % self.config) + for message in messages: + message = message.replace(u'"', ur'\"') + pomsg = u'msgid "%s"\nmsgstr ""\n\n' % message + pofile.write(pomsg.encode('utf-8')) + finally: + pofile.close() -- cgit v1.2.1 From 208f82e12918f216abc723b3a6fd79976f9cf8a6 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Fri, 4 Jun 2010 18:26:47 +0200 Subject: Prepare msgid for escaped sequences. --- sphinx/builders/intl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index dc6cbf25..506f9001 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -69,7 +69,8 @@ class MessageCatalogBuilder(Builder): try: pofile.write(POHEADER % self.config) for message in messages: - message = message.replace(u'"', ur'\"') + # message contains *one* line of text ready for translation + message = message.replace(u'\\', ur'\\').replace(u'"', ur'\"') pomsg = u'msgid "%s"\nmsgstr ""\n\n' % message pofile.write(pomsg.encode('utf-8')) finally: -- cgit v1.2.1 From 31e2ffd3dd8cdf42c5a2cdf5b96076e184e127bc Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Fri, 4 Jun 2010 18:38:16 +0200 Subject: Document basic workflow in gettext builder. --- sphinx/builders/intl.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 506f9001..660155f3 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -39,6 +39,9 @@ msgstr "" """[1:] class MessageCatalogBuilder(Builder): + """ + Builds gettext-style message catalogs (.pot files). + """ name = 'gettext' def init(self): @@ -54,6 +57,11 @@ class MessageCatalogBuilder(Builder): return def write_doc(self, docname, doctree): + """ + Store a document's translatable strings in the message catalog of its + section. For this purpose a document's *top-level directory* -- or + otherwise its *name* -- is considered its section. + """ catalog = self.catalogs[docname.split('/')[0]] for node in doctree.traverse(nodes.TextElement): if isinstance(node, (nodes.Invisible, nodes.Inline)): -- cgit v1.2.1 From ae927fd290828cc22f3c25f72505391e5063b3cd Mon Sep 17 00:00:00 2001 From: jacob Date: Fri, 4 Jun 2010 16:12:29 -0500 Subject: Made srcdir a kwarg. --- sphinx/websupport/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/websupport/api.py b/sphinx/websupport/api.py index eca24fb5..0d2722d6 100644 --- a/sphinx/websupport/api.py +++ b/sphinx/websupport/api.py @@ -16,7 +16,7 @@ from sphinx.application import Sphinx class WebSupport(object): - def init(self, srcdir, outdir=''): + def init(self, srcdir='', outdir=''): self.srcdir = srcdir self.outdir = outdir or path.join(self.srcdir, '_build', 'websupport') -- cgit v1.2.1 From c4fd97129431f73e400e95e7c30cf262bcf9e439 Mon Sep 17 00:00:00 2001 From: jacob Date: Fri, 4 Jun 2010 16:13:53 -0500 Subject: rough documentation --- doc/contents.rst | 1 + doc/web/api.rst | 26 ++++++++++++++++++ doc/web/quickstart.rst | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++ doc/websupport.rst | 9 +++++++ 4 files changed, 108 insertions(+) create mode 100644 doc/web/api.rst create mode 100644 doc/web/quickstart.rst create mode 100644 doc/websupport.rst diff --git a/doc/contents.rst b/doc/contents.rst index 079f93f2..1fb66711 100644 --- a/doc/contents.rst +++ b/doc/contents.rst @@ -17,6 +17,7 @@ Sphinx documentation contents theming templating extensions + websupport faq glossary diff --git a/doc/web/api.rst b/doc/web/api.rst new file mode 100644 index 00000000..a371fe3f --- /dev/null +++ b/doc/web/api.rst @@ -0,0 +1,26 @@ +.. _websupportapi: + +Web Support API +=============== + +.. module:: sphinx.websupport.api +.. class:: WebSupport + + The :class:`WebSupport` class provides a central interface for + working with :ref:`~sphinx.websupport.document.Document's. + +.. method:: init(srcdir='', outdir='') + + Initialize attributes. + +.. method:: get_document(docname) + + Retrieve the :class:`~sphinx.websupport.document.Document` object + corresponding to the *docname*. + +.. module:: sphinx.websupport.document +.. class:: Document + + The :class:`Document` provides access to a single document. It + is not instantiated directly, but is returned by methods of the + :class:`~sphinx.websupport.api.WebSupport` object. diff --git a/doc/web/quickstart.rst b/doc/web/quickstart.rst new file mode 100644 index 00000000..0a7094bf --- /dev/null +++ b/doc/web/quickstart.rst @@ -0,0 +1,72 @@ +.. _websupportquickstart: + +Web Support Quick Start +======================= + +To use the :ref:`websupportapi` in your application you must import +the :class:`~sphinx.websupport.api.WebSupport` object:: + + from sphinx.websupport import support + +This provides a reference to a :class:`~sphinx.websupport.api.WebSupport` +object. You will then need to provide some information about your +environment:: + + support.init(srcdir='/path/to/rst/sources/', + outdir='/path/to/build/outdir') + +You only need to provide a srcdir if you are building documentation:: + + support.build() + +This will create the data the web support package needs and place +it in *outdir*. You can then access +:class:`~sphinx.websupport.document.Document` objects by calling +the get_document(docname) method. For example, to retrieve the "contents" +document, do this:: + + contents_doc = support.get_document('contents') + +A more useful example, in the form of a `Flask `_ +application is:: + + from flask import Flask, render_template + from sphinx.websupport import support + + app = Flask(__name__) + + support.init(outdir='/path/to/sphinx/data') + + @app.route('/docs/') + def doc(docname): + document = support.get_document(docname) + return render_template('doc.html', document=document) + +This simple application will return a +:class:`~sphinx.websupport.document.Document` object corresponding +to the *docname* variable. This object will have *title* attribute, +as well as a list of HTML "slices". Each slice contains some HTML, +and when joined they form the body of a Sphinx document. Each slice +may or may not be commentable. If a slice is commentable, it will +have an *id* attribute which is used to associate a comment with +part of a document. + +In the previous example the doc.html template would look something +like this:: + + {% extends "base.html" %} + + {% block title %} + {{ document.title }} + {% endblock %} + + {% block body %} + {% for slice in document.slices -%} + {{ slice.html|safe }} + {% if slice.commentable -%} + + comment + + {%- endif %} + {%- endfor %} + {% endblock %} diff --git a/doc/websupport.rst b/doc/websupport.rst new file mode 100644 index 00000000..a7870460 --- /dev/null +++ b/doc/websupport.rst @@ -0,0 +1,9 @@ +.. _websupport: + +Sphinx Web Support +================== + +.. toctree:: + + web/quickstart + web/api \ No newline at end of file -- cgit v1.2.1 From 91910e092fe4330ac2998e7673a028f3d8c63317 Mon Sep 17 00:00:00 2001 From: jacob Date: Fri, 4 Jun 2010 16:46:06 -0500 Subject: updated CHANGES.jacobmason --- CHANGES.jacobmason | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.jacobmason b/CHANGES.jacobmason index 42adc427..fe7c57fb 100644 --- a/CHANGES.jacobmason +++ b/CHANGES.jacobmason @@ -1,2 +1,6 @@ May 30: Added files builders/websupport.py, writers/websupport.py, -websupport/api.py, and websupport/document.api. Provides a rudimentary method of building websupport data, and rendering it as html. \ No newline at end of file +websupport/api.py, and websupport/document.api. Provides a rudimentary +method of building websupport data, and rendering it as html. + +May 31-June 4: Continued changing way web support data is represented +and accessed. \ No newline at end of file -- cgit v1.2.1 From 6ce32bc0834c3128df949cc401302981605abeb0 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Sun, 6 Jun 2010 20:54:01 +0200 Subject: Supply version information in PO header. --- sphinx/builders/intl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 660155f3..199a5a3e 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -17,7 +17,7 @@ from docutils import nodes from sphinx.builders import Builder from sphinx.util.console import darkgreen -POHEADER = r""" +POHEADER = ur""" # SOME DESCRIPTIVE TITLE. # Copyright (C) %(copyright)s # This file is distributed under the same license as the %(project)s package. @@ -26,7 +26,7 @@ POHEADER = r""" #, fuzzy msgid "" msgstr "" -"Project-Id-Version: 1.0\n" +"Project-Id-Version: %(version)s\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2010-05-08 18:29+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -- cgit v1.2.1 From 914dda48502d0df78ea64e2f076981d8ae0a7de9 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Sun, 6 Jun 2010 20:55:57 +0200 Subject: Supply initial creation date in PO header. --- sphinx/builders/intl.py | 2 +- sphinx/config.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 199a5a3e..2fc5011c 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -28,7 +28,7 @@ msgid "" msgstr "" "Project-Id-Version: %(version)s\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-05-08 18:29+0200\n" +"POT-Creation-Date: %(gettext_ctime)s\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/sphinx/config.py b/sphinx/config.py index e2542782..ddaa3dfd 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -11,6 +11,7 @@ import os import re +from datetime import datetime from os import path from sphinx.errors import ConfigError @@ -150,6 +151,11 @@ class Config(object): # manpage options man_pages = ([], None), + + # gettext options + gettext_ctime = (lambda self:datetime.now() # should supply tz + .strftime('%Y-%m-%d %H:%M%z'), + 'gettext'), ) def __init__(self, dirname, filename, overrides, tags): -- cgit v1.2.1 From e614367d3c3a602683d8c37af1864082d5408ee3 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Mon, 7 Jun 2010 13:58:04 +0200 Subject: Add generic test for gettext builder. --- tests/test_build.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_build.py b/tests/test_build.py index f18ff175..6d98e399 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -57,3 +57,7 @@ else: @with_app(buildername='singlehtml', cleanenv=True) def test_singlehtml(app): app.builder.build_all() + +@with_app(buildername='gettext') +def test_gettext(app): + app.builder.build_all() -- cgit v1.2.1 From 371ca9a09c176ec724519f2f55d12560355426fd Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Mon, 7 Jun 2010 14:04:16 +0200 Subject: Initial tests for gettext build. --- tests/test_build_gettext.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/test_build_gettext.py diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py new file mode 100644 index 00000000..caa77587 --- /dev/null +++ b/tests/test_build_gettext.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +""" + test_build_gettext + ~~~~~~~~~~~~~~~~ + + Test the build process with gettext builder with the test root. + + :copyright: Copyright 2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from util import * + + +def teardown_module(): + (test_root / '_build').rmtree(True) + + +@with_app(buildername='gettext', cleanenv=True) +def test_gettext(app): + app.builder.build_all() + assert (app.outdir / 'contents.pot').isfile() + # group into sections + assert (app.outdir / 'subdir.pot').isfile() -- cgit v1.2.1 From 86b7377996325762783fd1df0e070e5ec77735e4 Mon Sep 17 00:00:00 2001 From: jacob Date: Mon, 7 Jun 2010 16:09:19 -0500 Subject: Now serves static body. --- doc/web/api.rst | 2 +- sphinx/builders/websupport.py | 25 ------------------------- sphinx/locale/__init__.py | 3 +++ sphinx/writers/websupport.py | 38 +++++++++++++++++--------------------- 4 files changed, 21 insertions(+), 47 deletions(-) diff --git a/doc/web/api.rst b/doc/web/api.rst index a371fe3f..66e89af3 100644 --- a/doc/web/api.rst +++ b/doc/web/api.rst @@ -7,7 +7,7 @@ Web Support API .. class:: WebSupport The :class:`WebSupport` class provides a central interface for - working with :ref:`~sphinx.websupport.document.Document's. + working with :class:`~sphinx.websupport.document.Document`'s. .. method:: init(srcdir='', outdir='') diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 48e895db..8bc94174 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -9,9 +9,6 @@ :license: BSD, see LICENSE for details. """ -from os import path - -from sphinx.util.osutil import ensuredir, os_path from sphinx.builders.html import PickleHTMLBuilder from sphinx.writers.websupport import WebSupportTranslator @@ -20,7 +17,6 @@ class WebSupportBuilder(PickleHTMLBuilder): Builds documents for the web support package. """ name = 'websupport' - template_suffix = '.html' def init_translator_class(self): self.translator_class = WebSupportTranslator @@ -30,26 +26,5 @@ class WebSupportBuilder(PickleHTMLBuilder): self.docname = docname PickleHTMLBuilder.write_doc(self, docname, doctree) - def handle_page(self, pagename, ctx, templatename='', **ignored): - # Mostly copied from PickleHTMLBuilder. - ctx['current_page_name'] = ctx['pagename'] = pagename - self.add_sidebars(pagename, ctx) - - self.app.emit('html-page-context', pagename, ctx) - - # Instead of pickling ctx as PickleHTMLBuilder does, we - # have created a Document object and pickle that. - document = self.docwriter.visitor.support_document - document.__dict__.update(ctx) - - doc_filename = path.join(self.outdir, - os_path(pagename) + self.out_suffix) - ensuredir(path.dirname(doc_filename)) - f = open(doc_filename, 'wb') - try: - self.implementation.dump(document, f) - finally: - f.close() - def get_target_uri(self, docname, typ=None): return docname diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py index badcca1c..43e0942c 100644 --- a/sphinx/locale/__init__.py +++ b/sphinx/locale/__init__.py @@ -32,6 +32,9 @@ class _TranslationProxy(UserString.UserString, object): return unicode(func) return object.__new__(cls) + def __getnewargs__(self): + return (self._func,) + self._args + def __init__(self, func, *args): self._func = func self._args = args diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index 6d255e64..d99d7dc9 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -10,7 +10,6 @@ """ from sphinx.writers.html import HTMLTranslator -from sphinx.websupport.document import Document class WebSupportTranslator(HTMLTranslator): """ @@ -23,7 +22,6 @@ class WebSupportTranslator(HTMLTranslator): self.init_support() def init_support(self): - self.support_document = Document() self.in_commentable = False self.current_id = 0 @@ -38,29 +36,27 @@ class WebSupportTranslator(HTMLTranslator): self.handle_depart_commentable(node) def handle_visit_commentable(self, node): - # If we are already recording a commentable slice we don't do - # anything. We can't handle nesting. - if not self.in_commentable: - self.support_document.add_slice(''.join(self.body)) - node.commented = self.in_commentable = True - self.body = [] - else: + # If this node is nested inside another commentable node this + # node will not be commented. + if self.in_commentable: node.commented = False + else: + node.commented = self.in_commentable = True + node.id = self.create_id(node) + # We will place the node in the HTML id attribute. If the node + # already has another id (for indexing purposes) put an empty + # span with the existing id directly before this node's HTML. + if node.attributes['ids']: + self.body.append('' + % node.attributes['ids'][0]) + node.attributes['ids'] = [node.id] + node.attributes['classes'].append('spxcmt') def handle_depart_commentable(self, node): assert(self.in_commentable) if node.commented: - slice_id = '%s-%s' % (self.builder.docname, self.current_id) - self.current_id += 1 - - body = ''.join(self.body) - self.support_document.add_slice(body, slice_id, commentable=True) - self.in_commentable = False - self.body = [] - - def depart_document(self, node): - assert(not self.in_commentable) - self.support_document.add_slice(''.join(self.body)) - + def create_id(self, node): + self.current_id += 1 + return '%s_%s' % (node.__class__.__name__, self.current_id) -- cgit v1.2.1 From 0b3da8fdc20936964bdffa08f6a6ec94436b4c41 Mon Sep 17 00:00:00 2001 From: jacob Date: Mon, 7 Jun 2010 18:19:40 -0500 Subject: removed document.py --- sphinx/websupport/document.py | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 sphinx/websupport/document.py diff --git a/sphinx/websupport/document.py b/sphinx/websupport/document.py deleted file mode 100644 index 16a60934..00000000 --- a/sphinx/websupport/document.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.websupport.document - ~~~~~~~~~~~~~~~~~~~~ - - Contains a Document class for working with Sphinx documents. - - :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -""" - -from os import path - -from jinja2 import Template -from docutils import nodes -from sphinx import addnodes - -class Document(object): - """A single Document such as 'index'.""" - def __init__(self): - self.slices = [] - - def add_slice(self, html, id=None, commentable=False): - slice = HTMLSlice(html, id, commentable) - self.slices.append(slice) - -class HTMLSlice(object): - def __init__(self, html, id, commentable): - self.html = html - self.id = id - self.commentable = commentable -- cgit v1.2.1 From 1bba75f5876c40fa0ef291258c0b44fc471d2882 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Wed, 9 Jun 2010 06:46:30 +0200 Subject: Verify PO file format with msginit. --- tests/test_build_gettext.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index caa77587..3a992e12 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -9,6 +9,9 @@ :license: BSD, see LICENSE for details. """ +import os +from subprocess import Popen, PIPE + from util import * @@ -22,3 +25,22 @@ def test_gettext(app): assert (app.outdir / 'contents.pot').isfile() # group into sections assert (app.outdir / 'subdir.pot').isfile() + + cwd = os.getcwd() + os.chdir(app.outdir) + try: + try: + p = Popen(['msginit', '--no-translator', '-i', 'contents.pot'], + stdout=PIPE, stderr=PIPE) + except OSError: + return # most likely msginit was not found + else: + stdout, stderr = p.communicate() + if p.returncode != 0: + print stdout + print stderr + del app.cleanup_trees[:] + assert False, 'msginit exited with return code %s' % p.returncode + assert (app.outdir / 'en_US.po').isfile(), 'msginit failed' + finally: + os.chdir(cwd) -- cgit v1.2.1 From 455b24a164924b29c339c11ee0b9114a81a0caad Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Wed, 9 Jun 2010 06:54:59 +0200 Subject: Prepare test root catalogs for gettext with msgfmt. --- tests/test_build_gettext.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index 3a992e12..72c00a0d 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -26,6 +26,7 @@ def test_gettext(app): # group into sections assert (app.outdir / 'subdir.pot').isfile() + (app.outdir / 'en' / 'LC_MESSAGES').makedirs() cwd = os.getcwd() os.chdir(app.outdir) try: @@ -42,5 +43,19 @@ def test_gettext(app): del app.cleanup_trees[:] assert False, 'msginit exited with return code %s' % p.returncode assert (app.outdir / 'en_US.po').isfile(), 'msginit failed' + try: + p = Popen(['msgfmt', 'en_US.po', '-o', + os.path.join('en', 'LC_MESSAGES', 'test_root.mo')], + stdout=PIPE, stderr=PIPE) + except OSError: + return # most likely msgfmt was not found + else: + stdout, stderr = p.communicate() + if p.returncode != 0: + print stdout + print stderr + del app.cleanup_trees[:] + assert False, 'msgfmt exited with return code %s' % p.returncode + assert (app.outdir / 'en' / 'LC_MESSAGES' / 'test_root.mo').isfile(), 'msgfmt failed' finally: os.chdir(cwd) -- cgit v1.2.1 From 61b69e6705eb733bd559ac1d7f1ff1f7c8bb099f Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Wed, 9 Jun 2010 07:46:40 +0200 Subject: Delete generated files on test failure. --- tests/test_build_gettext.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index 72c00a0d..5a794515 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -40,7 +40,6 @@ def test_gettext(app): if p.returncode != 0: print stdout print stderr - del app.cleanup_trees[:] assert False, 'msginit exited with return code %s' % p.returncode assert (app.outdir / 'en_US.po').isfile(), 'msginit failed' try: @@ -54,7 +53,6 @@ def test_gettext(app): if p.returncode != 0: print stdout print stderr - del app.cleanup_trees[:] assert False, 'msgfmt exited with return code %s' % p.returncode assert (app.outdir / 'en' / 'LC_MESSAGES' / 'test_root.mo').isfile(), 'msgfmt failed' finally: -- cgit v1.2.1 From bdfbc594b2277495cba35474187f717aa7f2088d Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Wed, 9 Jun 2010 07:47:14 +0200 Subject: Fix empty and duplicate nodes. --- sphinx/builders/intl.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 2fc5011c..4943fb29 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -66,7 +66,11 @@ class MessageCatalogBuilder(Builder): for node in doctree.traverse(nodes.TextElement): if isinstance(node, (nodes.Invisible, nodes.Inline)): continue - msg = node.astext().replace('\n', ' ') + msg = node.astext().replace('\n', ' ').strip() + # XXX nodes rendering empty are likely a bug in sphinx.addnodes + # XXX msgctxt for duplicate messages? + if not msg or msg in catalog: + continue catalog.append(msg) def finish(self): -- cgit v1.2.1 From 741965dd5f542ceac7d337393d268d3d18fa7b0c Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Wed, 9 Jun 2010 15:19:58 +0200 Subject: Refactor message extractor into utilities. --- sphinx/builders/intl.py | 14 +++++--------- sphinx/util/nodes.py | 11 +++++++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 4943fb29..72fb7896 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -15,6 +15,7 @@ from os import path from docutils import nodes from sphinx.builders import Builder +from sphinx.util.nodes import extract_messages from sphinx.util.console import darkgreen POHEADER = ur""" @@ -63,15 +64,10 @@ class MessageCatalogBuilder(Builder): otherwise its *name* -- is considered its section. """ catalog = self.catalogs[docname.split('/')[0]] - for node in doctree.traverse(nodes.TextElement): - if isinstance(node, (nodes.Invisible, nodes.Inline)): - continue - msg = node.astext().replace('\n', ' ').strip() - # XXX nodes rendering empty are likely a bug in sphinx.addnodes - # XXX msgctxt for duplicate messages? - if not msg or msg in catalog: - continue - catalog.append(msg) + for msg in extract_messages(doctree): + # XXX msgctxt for duplicate messages + if msg not in catalog: + catalog.append(msg) def finish(self): for section, messages in self.status_iterator( diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 82427f13..84182b15 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -22,6 +22,17 @@ explicit_title_re = re.compile(r'^(.+?)\s*(?$', re.DOTALL) caption_ref_re = explicit_title_re # b/w compat alias +def extract_messages(doctree): + """Extract translatable messages from a document tree.""" + for node in doctree.traverse(nodes.TextElement): + if isinstance(node, (nodes.Invisible, nodes.Inline)): + continue + msg = node.astext().replace('\n', ' ').strip() + # XXX nodes rendering empty are likely a bug in sphinx.addnodes + if msg: + yield msg + + def nested_parse_with_titles(state, content, node): # hack around title style bookkeeping surrounding_title_styles = state.memo.title_styles -- cgit v1.2.1 From b5e948d9622c27790074a771649a37acbc0454c8 Mon Sep 17 00:00:00 2001 From: jacob Date: Tue, 15 Jun 2010 22:10:36 -0500 Subject: Don't add attributes to node. --- doc/websupport.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/websupport.rst b/doc/websupport.rst index a7870460..2927f5a7 100644 --- a/doc/websupport.rst +++ b/doc/websupport.rst @@ -3,6 +3,10 @@ Sphinx Web Support ================== +Sphinx provides a way to easily integrate Sphinx documentation +into your web application. To learn more read the +:ref:`websupportquickstart`. + .. toctree:: web/quickstart -- cgit v1.2.1 From 38e65ecb4aacb5c844a3e554effa76bced752ae6 Mon Sep 17 00:00:00 2001 From: jacob Date: Tue, 15 Jun 2010 22:15:10 -0500 Subject: Don't add attributes to node. --- sphinx/writers/websupport.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index d99d7dc9..4afc3ecb 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -19,6 +19,7 @@ class WebSupportTranslator(HTMLTranslator): def __init__(self, builder, *args, **kwargs): HTMLTranslator.__init__(self, builder, *args, **kwargs) + self.comment_class = 'spxcmt' self.init_support() def init_support(self): @@ -38,23 +39,21 @@ class WebSupportTranslator(HTMLTranslator): def handle_visit_commentable(self, node): # If this node is nested inside another commentable node this # node will not be commented. - if self.in_commentable: - node.commented = False - else: - node.commented = self.in_commentable = True - node.id = self.create_id(node) + if not self.in_commentable: + self.in_commentable = True + id = self.create_id(node) # We will place the node in the HTML id attribute. If the node # already has another id (for indexing purposes) put an empty # span with the existing id directly before this node's HTML. if node.attributes['ids']: self.body.append('' % node.attributes['ids'][0]) - node.attributes['ids'] = [node.id] - node.attributes['classes'].append('spxcmt') + node.attributes['ids'] = [id] + node.attributes['classes'].append(self.comment_class) def handle_depart_commentable(self, node): assert(self.in_commentable) - if node.commented: + if self.comment_class in node.attributes['classes']: self.in_commentable = False def create_id(self, node): -- cgit v1.2.1 From 9e10d8f986614e60c2948b720e89b4ccdbe0f428 Mon Sep 17 00:00:00 2001 From: jacob Date: Wed, 16 Jun 2010 08:33:33 -0500 Subject: updated documentation --- doc/web/api.rst | 12 ++---------- doc/web/quickstart.rst | 45 +++++++++++++++++++++++++-------------------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/doc/web/api.rst b/doc/web/api.rst index 66e89af3..65bf0c58 100644 --- a/doc/web/api.rst +++ b/doc/web/api.rst @@ -7,7 +7,7 @@ Web Support API .. class:: WebSupport The :class:`WebSupport` class provides a central interface for - working with :class:`~sphinx.websupport.document.Document`'s. + working Sphinx documentation. .. method:: init(srcdir='', outdir='') @@ -15,12 +15,4 @@ Web Support API .. method:: get_document(docname) - Retrieve the :class:`~sphinx.websupport.document.Document` object - corresponding to the *docname*. - -.. module:: sphinx.websupport.document -.. class:: Document - - The :class:`Document` provides access to a single document. It - is not instantiated directly, but is returned by methods of the - :class:`~sphinx.websupport.api.WebSupport` object. + Retrieve the context dictionary corresponding to the *docname*. diff --git a/doc/web/quickstart.rst b/doc/web/quickstart.rst index 0a7094bf..94dfb576 100644 --- a/doc/web/quickstart.rst +++ b/doc/web/quickstart.rst @@ -20,13 +20,15 @@ You only need to provide a srcdir if you are building documentation:: support.build() This will create the data the web support package needs and place -it in *outdir*. You can then access -:class:`~sphinx.websupport.document.Document` objects by calling -the get_document(docname) method. For example, to retrieve the "contents" +it in *outdir*. You can then access this data by calling the +get_document(docname) method. For example, to retrieve the "contents" document, do this:: contents_doc = support.get_document('contents') +This will return a dictionary containing the context you need to render +a document. + A more useful example, in the form of a `Flask `_ application is:: @@ -42,15 +44,6 @@ application is:: document = support.get_document(docname) return render_template('doc.html', document=document) -This simple application will return a -:class:`~sphinx.websupport.document.Document` object corresponding -to the *docname* variable. This object will have *title* attribute, -as well as a list of HTML "slices". Each slice contains some HTML, -and when joined they form the body of a Sphinx document. Each slice -may or may not be commentable. If a slice is commentable, it will -have an *id* attribute which is used to associate a comment with -part of a document. - In the previous example the doc.html template would look something like this:: @@ -60,13 +53,25 @@ like this:: {{ document.title }} {% endblock %} + {% block extra_js %} + + + {% endblock %} + {% block body %} - {% for slice in document.slices -%} - {{ slice.html|safe }} - {% if slice.commentable -%} - - comment - - {%- endif %} - {%- endfor %} + {{ document.body|safe }} + {% endblock %} + + {% block sidebar %} {% endblock %} -- cgit v1.2.1 From c794dbb0ed7fcc01bfdf60d02201384a7ff0f45d Mon Sep 17 00:00:00 2001 From: jacob Date: Wed, 16 Jun 2010 08:41:43 -0500 Subject: fixed typo in docs --- doc/web/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/web/api.rst b/doc/web/api.rst index 65bf0c58..13df6a40 100644 --- a/doc/web/api.rst +++ b/doc/web/api.rst @@ -7,7 +7,7 @@ Web Support API .. class:: WebSupport The :class:`WebSupport` class provides a central interface for - working Sphinx documentation. + working with Sphinx documentation. .. method:: init(srcdir='', outdir='') -- cgit v1.2.1 From 88035b10fae8746674f794260ffbdf2114bb7d1d Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Wed, 16 Jun 2010 20:27:27 +0200 Subject: Extract translatable strings alongside their doctree nodes. --- sphinx/builders/intl.py | 2 +- sphinx/util/nodes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 72fb7896..b760854b 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -64,7 +64,7 @@ class MessageCatalogBuilder(Builder): otherwise its *name* -- is considered its section. """ catalog = self.catalogs[docname.split('/')[0]] - for msg in extract_messages(doctree): + for _, msg in extract_messages(doctree): # XXX msgctxt for duplicate messages if msg not in catalog: catalog.append(msg) diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 84182b15..0b23d17f 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -30,7 +30,7 @@ def extract_messages(doctree): msg = node.astext().replace('\n', ' ').strip() # XXX nodes rendering empty are likely a bug in sphinx.addnodes if msg: - yield msg + yield node, msg def nested_parse_with_titles(state, content, node): -- cgit v1.2.1 From 48ea016b78d73506986b4293040722454d0b0780 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Thu, 17 Jun 2010 11:46:49 +0200 Subject: Split up tests into logical units. --- tests/test_build_gettext.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index 5a794515..ee18d96e 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -20,12 +20,17 @@ def teardown_module(): @with_app(buildername='gettext', cleanenv=True) -def test_gettext(app): +def test_build(app): app.builder.build_all() + # documents end up in a message catalog assert (app.outdir / 'contents.pot').isfile() - # group into sections + # ..and are grouped into sections assert (app.outdir / 'subdir.pot').isfile() +@with_app(buildername='gettext', cleanenv=True) +def test_gettext(app): + app.builder.build_all() + (app.outdir / 'en' / 'LC_MESSAGES').makedirs() cwd = os.getcwd() os.chdir(app.outdir) -- cgit v1.2.1 From ebb9bab1040759027f01ac3d97c3dacff7e35b3d Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Thu, 17 Jun 2010 11:49:07 +0200 Subject: Remove cleanenv setting from tests. --- tests/test_build_gettext.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index ee18d96e..23655835 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -19,7 +19,7 @@ def teardown_module(): (test_root / '_build').rmtree(True) -@with_app(buildername='gettext', cleanenv=True) +@with_app(buildername='gettext') def test_build(app): app.builder.build_all() # documents end up in a message catalog @@ -27,7 +27,7 @@ def test_build(app): # ..and are grouped into sections assert (app.outdir / 'subdir.pot').isfile() -@with_app(buildername='gettext', cleanenv=True) +@with_app(buildername='gettext') def test_gettext(app): app.builder.build_all() -- cgit v1.2.1 From 7614fa79c39d353aecc47e2ce4f9bb0cd33d17f8 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Thu, 17 Jun 2010 11:57:59 +0200 Subject: Strip down tests to build only critical parts. --- tests/test_build_gettext.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index 23655835..c0dff938 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -21,22 +21,22 @@ def teardown_module(): @with_app(buildername='gettext') def test_build(app): - app.builder.build_all() + app.builder.build(['extapi', 'subdir/includes']) # documents end up in a message catalog - assert (app.outdir / 'contents.pot').isfile() + assert (app.outdir / 'extapi.pot').isfile() # ..and are grouped into sections assert (app.outdir / 'subdir.pot').isfile() @with_app(buildername='gettext') def test_gettext(app): - app.builder.build_all() + app.builder.build(['markup']) (app.outdir / 'en' / 'LC_MESSAGES').makedirs() cwd = os.getcwd() os.chdir(app.outdir) try: try: - p = Popen(['msginit', '--no-translator', '-i', 'contents.pot'], + p = Popen(['msginit', '--no-translator', '-i', 'markup.pot'], stdout=PIPE, stderr=PIPE) except OSError: return # most likely msginit was not found -- cgit v1.2.1 From 42189c1325f3d1c0d4a1706b87199d7dc5a51ecd Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Fri, 18 Jun 2010 10:15:08 +0200 Subject: Patch translatable messages with custom doctree. --- sphinx/environment.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/sphinx/environment.py b/sphinx/environment.py index c8b3f018..acf76f6e 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -24,7 +24,7 @@ from itertools import izip, groupby from docutils import nodes from docutils.io import FileInput, NullOutput from docutils.core import Publisher -from docutils.utils import Reporter, relative_path +from docutils.utils import Reporter, relative_path, new_document from docutils.readers import standalone from docutils.parsers.rst import roles, directives from docutils.parsers.rst.languages import en as english @@ -36,7 +36,7 @@ from docutils.transforms.parts import ContentsFilter from sphinx import addnodes from sphinx.util import url_re, get_matching_docs, docname_join, \ FilenameUniqDict -from sphinx.util.nodes import clean_astext, make_refnode +from sphinx.util.nodes import clean_astext, make_refnode, extract_messages from sphinx.util.osutil import movefile, SEP, ustrftime from sphinx.util.matching import compile_matchers from sphinx.errors import SphinxError, ExtensionError @@ -168,13 +168,26 @@ class CitationReferences(Transform): refnode += nodes.Text('[' + cittext + ']') citnode.parent.replace(citnode, refnode) +class Locale(Transform): + """ + Replace translatable nodes with their translated doctree. + """ + default_priority = 0 + + def apply(self): + settings = self.document.settings + for node, msg in extract_messages(self.document): + ctx = node.parent + patch = new_document(msg, settings) + ctx.replace(node, patch.children) + class SphinxStandaloneReader(standalone.Reader): """ Add our own transforms. """ - transforms = [CitationReferences, DefaultSubstitutions, MoveModuleTargets, - HandleCodeBlocks, SortIds] + transforms = [Locale, CitationReferences, DefaultSubstitutions, + MoveModuleTargets, HandleCodeBlocks, SortIds] def get_transforms(self): return standalone.Reader.get_transforms(self) + self.transforms -- cgit v1.2.1 From 1d8dd2941f5c2f9fee83fb0c9a585a4009e3cede Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Wed, 23 Jun 2010 06:18:47 +0200 Subject: Check compiled message catalogs are processable with gettext. --- tests/test_build_gettext.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index c0dff938..7041dcac 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -9,6 +9,7 @@ :license: BSD, see LICENSE for details. """ +import gettext import os from subprocess import Popen, PIPE @@ -62,3 +63,6 @@ def test_gettext(app): assert (app.outdir / 'en' / 'LC_MESSAGES' / 'test_root.mo').isfile(), 'msgfmt failed' finally: os.chdir(cwd) + + _ = gettext.translation('test_root', app.outdir, languages=['en']).ugettext + assert _("Testing various markup") == u"Testing various markup" -- cgit v1.2.1 From 54d2c46be16559cd7a486e38bd4699c03694d29d Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Wed, 23 Jun 2010 07:28:58 +0200 Subject: Add parsing step to translation integration. --- sphinx/environment.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sphinx/environment.py b/sphinx/environment.py index acf76f6e..cab03cd2 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -26,7 +26,7 @@ from docutils.io import FileInput, NullOutput from docutils.core import Publisher from docutils.utils import Reporter, relative_path, new_document from docutils.readers import standalone -from docutils.parsers.rst import roles, directives +from docutils.parsers.rst import roles, directives, Parser as RSTParser from docutils.parsers.rst.languages import en as english from docutils.parsers.rst.directives.html import MetaBody from docutils.writers import UnfilteredWriter @@ -176,9 +176,12 @@ class Locale(Transform): def apply(self): settings = self.document.settings + parser = RSTParser() for node, msg in extract_messages(self.document): ctx = node.parent patch = new_document(msg, settings) + msgstr = "Insert translation **here**." + parser.parse(msgstr, patch) ctx.replace(node, patch.children) -- cgit v1.2.1 From b7eca36b9e2bd1067f8ed2693898e45b65e4abc4 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Wed, 23 Jun 2010 07:30:24 +0200 Subject: Fix source file reference in patched documents. --- sphinx/environment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/environment.py b/sphinx/environment.py index cab03cd2..a2c30ddd 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -175,11 +175,11 @@ class Locale(Transform): default_priority = 0 def apply(self): - settings = self.document.settings + settings, source = self.document.settings, self.document['source'] parser = RSTParser() for node, msg in extract_messages(self.document): ctx = node.parent - patch = new_document(msg, settings) + patch = new_document(source, settings) msgstr = "Insert translation **here**." parser.parse(msgstr, patch) ctx.replace(node, patch.children) -- cgit v1.2.1 From e07c695f1410bc9b8577af03a0397d2ddb032446 Mon Sep 17 00:00:00 2001 From: Robert Lehmann Date: Wed, 23 Jun 2010 07:36:33 +0200 Subject: Ignore orphan metadata field in translatable messages. --- sphinx/util/nodes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 0b23d17f..87fd362c 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -27,6 +27,9 @@ def extract_messages(doctree): for node in doctree.traverse(nodes.TextElement): if isinstance(node, (nodes.Invisible, nodes.Inline)): continue + # orphan + if isinstance(node, nodes.field_name) and node.children[0] == 'orphan': + continue msg = node.astext().replace('\n', ' ').strip() # XXX nodes rendering empty are likely a bug in sphinx.addnodes if msg: -- cgit v1.2.1 From 85bf90cec4879955d92be7503011217fa94b4c3b Mon Sep 17 00:00:00 2001 From: jacob Date: Wed, 23 Jun 2010 14:37:07 -0500 Subject: Added relbar and sidebar to documents --- sphinx/builders/websupport.py | 67 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 8bc94174..55b90e68 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -9,22 +9,81 @@ :license: BSD, see LICENSE for details. """ -from sphinx.builders.html import PickleHTMLBuilder +import cPickle as pickle +from os import path + +from sphinx.util.osutil import os_path, relative_uri, ensuredir, copyfile +from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.writers.websupport import WebSupportTranslator -class WebSupportBuilder(PickleHTMLBuilder): +class WebSupportBuilder(StandaloneHTMLBuilder): """ Builds documents for the web support package. """ name = 'websupport' - + out_suffix = '.fpickle' + def init_translator_class(self): self.translator_class = WebSupportTranslator def write_doc(self, docname, doctree): # The translator needs the docname to generate ids. self.docname = docname - PickleHTMLBuilder.write_doc(self, docname, doctree) + StandaloneHTMLBuilder.write_doc(self, docname, doctree) def get_target_uri(self, docname, typ=None): return docname + + def handle_page(self, pagename, addctx, templatename='page.html', + outfilename=None, event_arg=None): + # This is mostly copied from StandaloneHTMLBuilder. However, instead + # of rendering the template and saving the html, create a context + # dict and pickle it. + ctx = self.globalcontext.copy() + ctx['pagename'] = pagename + + def pathto(otheruri, resource=False, + baseuri=self.get_target_uri(pagename)): + if not resource: + otheruri = self.get_target_uri(otheruri) + uri = relative_uri(baseuri, otheruri) or '#' + return uri + ctx['pathto'] = pathto + ctx['hasdoc'] = lambda name: name in self.env.all_docs + ctx['encoding'] = encoding = self.config.html_output_encoding + ctx['toctree'] = lambda **kw: self._get_local_toctree(pagename, **kw) + self.add_sidebars(pagename, ctx) + ctx.update(addctx) + + self.app.emit('html-page-context', pagename, templatename, + ctx, event_arg) + + # Create a dict that will be pickled and used by webapps. + doc_ctx = {'body': ctx.get('body', '')} + # Partially render the html template to proved a more useful ctx. + template = self.templates.environment.get_template(templatename) + template_module = template.make_module(ctx) + if hasattr(template_module, 'sidebar'): + doc_ctx['sidebar'] = template_module.sidebar() + if hasattr(template_module, 'relbar'): + doc_ctx['relbar'] = template_module.relbar() + + if not outfilename: + outfilename = path.join(self.outdir, + os_path(pagename) + self.out_suffix) + + ensuredir(path.dirname(outfilename)) + f = open(outfilename, 'wb') + try: + pickle.dump(doc_ctx, f, 2) + finally: + f.close() + + # if there is a source file, copy the source file for the + # "show source" link + if ctx.get('sourcename'): + source_name = path.join(self.outdir, '_sources', + os_path(ctx['sourcename'])) + ensuredir(path.dirname(source_name)) + copyfile(self.env.doc2path(pagename), source_name) + -- cgit v1.2.1 From 1744fed3ef408ab9dae3a66867a50962219fbd37 Mon Sep 17 00:00:00 2001 From: jacob Date: Thu, 24 Jun 2010 14:30:02 -0500 Subject: Added xapian search --- sphinx/builders/websupport.py | 18 +++++++- sphinx/themes/basic/searchresults.html | 36 +++++++++++++++ sphinx/websupport/api.py | 44 ++++++++++++++++-- sphinx/websupport/search/__init__.py | 36 +++++++++++++++ sphinx/websupport/search/xapiansearch.py | 76 ++++++++++++++++++++++++++++++++ 5 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 sphinx/themes/basic/searchresults.html create mode 100644 sphinx/websupport/search/__init__.py create mode 100644 sphinx/websupport/search/xapiansearch.py diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 55b90e68..8972c547 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -23,12 +23,26 @@ class WebSupportBuilder(StandaloneHTMLBuilder): name = 'websupport' out_suffix = '.fpickle' + def init(self): + self.init_search() + StandaloneHTMLBuilder.init(self) + + def init_search(self): + self.search = self.app.search + if self.search is not None: + self.search.create_index() + def init_translator_class(self): self.translator_class = WebSupportTranslator def write_doc(self, docname, doctree): # The translator needs the docname to generate ids. self.docname = docname + # Index the page if search is enabled. + if self.search is not None: + doc_contents = doctree.astext() + title = doc_contents[:20] + self.search.add_document(docname, title, doc_contents) StandaloneHTMLBuilder.write_doc(self, docname, doctree) def get_target_uri(self, docname, typ=None): @@ -59,7 +73,8 @@ class WebSupportBuilder(StandaloneHTMLBuilder): ctx, event_arg) # Create a dict that will be pickled and used by webapps. - doc_ctx = {'body': ctx.get('body', '')} + doc_ctx = {'body': ctx.get('body', ''), + 'title': ctx.get('title', '')} # Partially render the html template to proved a more useful ctx. template = self.templates.environment.get_template(templatename) template_module = template.make_module(ctx) @@ -86,4 +101,3 @@ class WebSupportBuilder(StandaloneHTMLBuilder): os_path(ctx['sourcename'])) ensuredir(path.dirname(source_name)) copyfile(self.env.doc2path(pagename), source_name) - diff --git a/sphinx/themes/basic/searchresults.html b/sphinx/themes/basic/searchresults.html new file mode 100644 index 00000000..0fec38de --- /dev/null +++ b/sphinx/themes/basic/searchresults.html @@ -0,0 +1,36 @@ +{# + basic/searchresults.html + ~~~~~~~~~~~~~~~~~ + + Template for the body of the search results page. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +#} +

    Search

    +

    + From here you can search these documents. Enter your search + words into the box below and click "search". +

    +
    + + + +
    +{% if search_performed %} +

    Search Results

    +{% if not search_results %} +

    'Your search did not match any results.

    +{% endif %} +{% endif %} +
    + {% if search_results %} +
      + {% for href, caption, context in search_results %} +
    • {{ caption }} +
      {{ context|e }}
      +
    • + {% endfor %} +
    + {% endif %} +
    diff --git a/sphinx/websupport/api.py b/sphinx/websupport/api.py index 0d2722d6..cc5f2f50 100644 --- a/sphinx/websupport/api.py +++ b/sphinx/websupport/api.py @@ -12,20 +12,47 @@ import cPickle as pickle from os import path +from jinja2 import Environment, FileSystemLoader + from sphinx.application import Sphinx +from sphinx.websupport.search import search_adapters + +class WebSupportApp(Sphinx): + def __init__(self, *args, **kwargs): + self.search = kwargs.pop('search', None) + Sphinx.__init__(self, *args, **kwargs) class WebSupport(object): - - def init(self, srcdir='', outdir=''): + def init(self, srcdir='', outdir='', search=None): self.srcdir = srcdir self.outdir = outdir or path.join(self.srcdir, '_build', 'websupport') + self.init_templating() + if search is not None: + self.init_search(search) + + def init_templating(self): + import sphinx + template_path = path.join(path.dirname(sphinx.__file__), + 'themes', 'basic') + loader = FileSystemLoader(template_path) + self.template_env = Environment(loader=loader) + + def init_search(self, search): + mod, cls = search_adapters[search] + search_class = getattr(__import__('sphinx.websupport.search.' + mod, + None, None, [cls]), cls) + search_path = path.join(self.outdir, 'search') + self.search = search_class(search_path) + self.results_template = \ + self.template_env.get_template('searchresults.html') def build(self, **kwargs): doctreedir = kwargs.pop('doctreedir', path.join(self.outdir, 'doctrees')) - app = Sphinx(self.srcdir, self.srcdir, - self.outdir, doctreedir, 'websupport') + app = WebSupportApp(self.srcdir, self.srcdir, + self.outdir, doctreedir, 'websupport', + search=self.search) app.build() def get_document(self, docname): @@ -33,3 +60,12 @@ class WebSupport(object): f = open(infilename, 'rb') document = pickle.load(f) return document + + def get_search_results(self, q): + results, results_found, results_displayed = self.search.query(q) + ctx = {'search_performed': True, + 'search_results': results} + document = self.get_document('search') + document['body'] = self.results_template.render(ctx) + document['title'] = 'Search Results' + return document diff --git a/sphinx/websupport/search/__init__.py b/sphinx/websupport/search/__init__.py new file mode 100644 index 00000000..ae82005a --- /dev/null +++ b/sphinx/websupport/search/__init__.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +""" + sphinx.websupport.search + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Server side search support for the web support package. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import re + +class BaseSearch(object): + def create_index(self, path): + raise NotImplemented + + def add_document(self, path, title, text): + raise NotImplemented + + def query(self, q): + raise NotImplemented + + def extract_context(self, text, query_string): + # From GSOC 2009 + with_context_re = '([\W\w]{0,80})(%s)([\W\w]{0,80})' % (query_string) + try: + res = re.findall(with_context_re, text, re.I|re.U)[0] + return tuple((unicode(i, errors='ignore') for i in res)) + except IndexError: + return '', '', '' + +search_adapters = { + 'xapian': ('xapiansearch', 'XapianSearch'), + 'whoosh': ('whooshsearch', 'WhooshSearch'), + } diff --git a/sphinx/websupport/search/xapiansearch.py b/sphinx/websupport/search/xapiansearch.py new file mode 100644 index 00000000..746a644d --- /dev/null +++ b/sphinx/websupport/search/xapiansearch.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +""" + sphinx.websupport.search.xapian + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Xapian search adapter. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from os import path + +import xapian + +from sphinx.util.osutil import ensuredir +from sphinx.websupport.search import BaseSearch + +class XapianSearch(BaseSearch): + # Adapted from the GSOC 2009 webapp project. + + # Xapian metadata constants + DOC_PATH = 0 + DOC_TITLE = 1 + + def __init__(self, db_path): + self.db_path = db_path + + def create_index(self): + ensuredir(self.db_path) + self.database = xapian.WritableDatabase(self.db_path, + xapian.DB_CREATE_OR_OPEN) + self.indexer = xapian.TermGenerator() + stemmer = xapian.Stem("english") + self.indexer.set_stemmer(stemmer) + + def add_document(self, path, title, text): + self.database.begin_transaction() + doc = xapian.Document() + doc.set_data(text) + doc.add_value(self.DOC_PATH, path) + doc.add_value(self.DOC_TITLE, title) + self.indexer.set_document(doc) + self.indexer.index_text(text) + for word in text.split(): + doc.add_posting(word, 1) + self.database.add_document(doc) + self.database.commit_transaction() + + def query(self, q): + database = xapian.Database(self.db_path) + enquire = xapian.Enquire(database) + qp = xapian.QueryParser() + stemmer = xapian.Stem("english") + qp.set_stemmer(stemmer) + qp.set_database(database) + qp.set_stemming_strategy(xapian.QueryParser.STEM_SOME) + query = qp.parse_query(q) + + # Find the top 100 results for the query. + enquire.set_query(query) + matches = enquire.get_mset(0, 100) + + results_found = matches.get_matches_estimated() + results_displayed = matches.size() + + results = [] + + for m in matches: + context = self.extract_context(m.document.get_data(), q) + results.append((m.document.get_value(self.DOC_PATH), + m.document.get_value(self.DOC_TITLE), + ''.join(context) )) + + return results, results_found, results_displayed + -- cgit v1.2.1 From 0b3b658bb998eb9d258d4570533a57335b03d57d Mon Sep 17 00:00:00 2001 From: Jacob Mason Date: Fri, 25 Jun 2010 14:37:46 -0500 Subject: Fixed styling problems in search results. --- sphinx/themes/basic/searchresults.html | 4 ++-- sphinx/websupport/api.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/sphinx/themes/basic/searchresults.html b/sphinx/themes/basic/searchresults.html index 0fec38de..e7fc84f8 100644 --- a/sphinx/themes/basic/searchresults.html +++ b/sphinx/themes/basic/searchresults.html @@ -25,9 +25,9 @@ {% endif %}
    {% if search_results %} -
      +
    +Now that this is done it's time to define the functions that handle +the AJAX calls from the script. You will need three functions. The first +function is used to add a new comment, and will call the web support method +:meth:`~sphinx.websupport.WebSupport.add_comment`:: + + @app.route('/docs/add_comment', methods=['POST']) + def add_comment(): + parent_id = request.form.get('parent', '') + text = request.form.get('text', '') + username = g.user.name if g.user is not None else 'Anonymous' + comment = support.add_comment(parent_id, text, username=username) + return jsonify(comment=comment) + +Then next function handles the retrieval of comments for a specific node, +and is aptly named :meth:`~sphinx.websupport.WebSupport.get_comments`:: + + @app.route('/docs/get_comments') + def get_comments(): + user_id = g.user.id if g.user else None + parent_id = request.args.get('parent', '') + comments = support.get_comments(parent_id, user_id) + return jsonify(comments=comments) + +The final function that is needed will call +:meth:`~sphinx.websupport.WebSupport.process_vote`, and will handle user +votes on comments:: + + @app.route('/docs/process_vote', methods=['POST']) + def process_vote(): + if g.user is None: + abort(401) + comment_id = request.form.get('comment_id') + value = request.form.get('value') + if value is None or comment_id is None: + abort(400) + support.process_vote(comment_id, g.user.id, value) + return "success" + +.. note:: + + Authentication is left up to your existing web application. If you do + not have an existing authentication system there are many readily + available for different frameworks. The web support system stores only + the user's unique integer `user_id` and uses this both for storing votes + and retrieving vote information. It is up to you to ensure that the + user_id passed in is unique, and that the user is authenticated. The + default backend will only allow one vote per comment per `user_id`. diff --git a/doc/websupport.rst b/doc/websupport.rst index 2927f5a7..e8fc238b 100644 --- a/doc/websupport.rst +++ b/doc/websupport.rst @@ -10,4 +10,5 @@ into your web application. To learn more read the .. toctree:: web/quickstart - web/api \ No newline at end of file + web/api + web/frontend \ No newline at end of file -- cgit v1.2.1 From 506dd5cf65c1e33fbc54c602abc5e0097ef2c887 Mon Sep 17 00:00:00 2001 From: Jacob Mason Date: Mon, 12 Jul 2010 12:41:32 -0500 Subject: added frontend.rst --- doc/web/frontend.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 doc/web/frontend.rst diff --git a/doc/web/frontend.rst b/doc/web/frontend.rst new file mode 100644 index 00000000..5ffe1667 --- /dev/null +++ b/doc/web/frontend.rst @@ -0,0 +1,6 @@ +.. _websupportfrontend: + +Web Support Frontend +==================== + +More coming soon. \ No newline at end of file -- cgit v1.2.1 From f0064da9c6903ead3c4504ccb24fbfd21b29880d Mon Sep 17 00:00:00 2001 From: Jacob Mason Date: Mon, 12 Jul 2010 14:25:08 -0500 Subject: updated CHANGES.jacobmason --- CHANGES.jacobmason | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGES.jacobmason b/CHANGES.jacobmason index fe7c57fb..bd87c71c 100644 --- a/CHANGES.jacobmason +++ b/CHANGES.jacobmason @@ -2,5 +2,14 @@ May 30: Added files builders/websupport.py, writers/websupport.py, websupport/api.py, and websupport/document.api. Provides a rudimentary method of building websupport data, and rendering it as html. -May 31-June 4: Continued changing way web support data is represented -and accessed. \ No newline at end of file +May 31-June 10: Continued changing way web support data is represented +and accessed. + +June 14 - June 17: Continued making improvements to the web support package +and demo web application. Included sidebars, navlinks etc... + +June 21 - June 26: Implement server side search with two search adapters, +one for Xapian and one for Whoosh + +June 28 - July 12: Implement voting system on the backend, and created a +jQuery script to handle voting on the frontend. \ No newline at end of file -- cgit v1.2.1 From cc9d8cb741fde240843de425640200aa7cf89758 Mon Sep 17 00:00:00 2001 From: Jacob Mason Date: Tue, 13 Jul 2010 15:33:12 -0500 Subject: API tweaks and more documentation --- doc/websupport.rst | 3 +- sphinx/websupport/__init__.py | 15 +++--- sphinx/websupport/search/__init__.py | 88 ++++++++++++++++++++++++++++---- sphinx/websupport/search/whooshsearch.py | 8 +-- sphinx/websupport/search/xapiansearch.py | 6 +-- 5 files changed, 95 insertions(+), 25 deletions(-) diff --git a/doc/websupport.rst b/doc/websupport.rst index e8fc238b..1b6725df 100644 --- a/doc/websupport.rst +++ b/doc/websupport.rst @@ -11,4 +11,5 @@ into your web application. To learn more read the web/quickstart web/api - web/frontend \ No newline at end of file + web/frontend + web/searchadapters \ No newline at end of file diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 2dbbe319..407cb4c9 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -17,7 +17,7 @@ from jinja2 import Environment, FileSystemLoader from sphinx.application import Sphinx from sphinx.util.osutil import ensuredir -from sphinx.websupport.search import search_adapters +from sphinx.websupport.search import BaseSearch, search_adapters from sphinx.websupport import comments as sphinxcomments class WebSupportApp(Sphinx): @@ -66,11 +66,14 @@ class WebSupport(object): self.template_env = Environment(loader=loader) def _init_search(self, search): - mod, cls = search_adapters[search] - search_class = getattr(__import__('sphinx.websupport.search.' + mod, + if isinstance(search, BaseSearch): + self.search = search + else: + mod, cls = search_adapters[search] + search_class = getattr(__import__('sphinx.websupport.search.' + mod, None, None, [cls]), cls) - search_path = path.join(self.outdir, 'search') - self.search = search_class(search_path) + search_path = path.join(self.outdir, 'search') + self.search = search_class(search_path) self.results_template = \ self.template_env.get_template('searchresults.html') @@ -133,7 +136,7 @@ class WebSupport(object): :param q: the search query """ - results, results_found, results_displayed = self.search.query(q) + results = self.search.query(q) ctx = {'search_performed': True, 'search_results': results, 'q': q} diff --git a/sphinx/websupport/search/__init__.py b/sphinx/websupport/search/__init__.py index b4bf7386..1886776a 100644 --- a/sphinx/websupport/search/__init__.py +++ b/sphinx/websupport/search/__init__.py @@ -13,39 +13,107 @@ import re class BaseSearch(object): def init_indexing(self, changed=[]): + """Called by the builder to initialize the search indexer. `changed` + is a list of pagenames that will be reindexed. You may want to remove + these from the search index before indexing begins. + + `param changed` is a list of pagenames that will be re-indexed + """ pass def finish_indexing(self): + """Called by the builder when writing has been completed. Use this + to perform any finalization or cleanup actions after indexing is + complete. + """ pass def feed(self, pagename, title, doctree): + """Called by the builder to add a doctree to the index. Converts the + `doctree` to text and passes it to :meth:`add_document`. You probably + won't want to override this unless you need access to the `doctree`. + Override :meth:`add_document` instead. + + `pagename` is the name of the page to be indexed + + `title` is the title of the page to be indexed + + `doctree` is the docutils doctree representation of the page + """ self.add_document(pagename, title, doctree.astext()) - def add_document(self, path, title, text): - raise NotImplemented + def add_document(self, pagename, title, text): + """Called by :meth:`feed` to add a document to the search index. + This method should should do everything necessary to add a single + document to the search index. + + `pagename` is name of the page being indexed. + It is the combination of the source files relative path and filename, + minus the extension. For example, if the source file is + "ext/builders.rst", the `pagename` would be "ext/builders". This + will need to be returned with search results when processing a + query. + + `title` is the page's title, and will need to be returned with + search results. + + `text` is the full text of the page. You probably want to store this + somehow to use while creating the context for search results. + """ + raise NotImplementedError() def query(self, q): + """Called by the web support api to get search results. This method + compiles the regular expression to be used when + :meth:`extracting context `, then calls + :meth:`handle_query`. You won't want to override this unless you + don't want to use the included :meth:`extract_context` method. + Override :meth:`handle_query` instead. + + `q` is the search query string. + """ self.context_re = re.compile('|'.join(q.split()), re.I) return self.handle_query(q) def handle_query(self, q): - raise NotImplemented + """Called by :meth:`query` to retrieve search results for a search + query `q`. This should return an iterable containing tuples of the + following format:: + + (, <context>) + + `path` and `title` are the same values that were passed to + :meth:`add_document`, and `context` should be a short text snippet + of the text surrounding the search query in the document. + + The :meth:`extract_context` method is provided as a simple way + to create the `context`. + """ + raise NotImplementedError() - def extract_context(self, text, query_string): + def extract_context(self, text, length=240): + """Extract the context for the search query from the documents + full `text`. + + `text` is the full text of the document to create the context for. + + `length` is the length of the context snippet to return. + """ res = self.context_re.search(text) if res is None: return '' - start = max(res.start() - 120, 0) - end = start + 240 - context = ''.join(['...' if start > 0 else '', - text[start:end], - '...' if end < len(text) else '']) + context_start = max(res.start() - length/2, 0) + context_end = start + length + context = ''.join(['...' if context_start > 0 else '', + text[context_start:context_end], + '...' if context_end < len(text) else '']) try: return unicode(context, errors='ignore') except TypeError: return context - + +# The build in search adapters. search_adapters = { 'xapian': ('xapiansearch', 'XapianSearch'), 'whoosh': ('whooshsearch', 'WhooshSearch'), diff --git a/sphinx/websupport/search/whooshsearch.py b/sphinx/websupport/search/whooshsearch.py index 991d4232..00c7403c 100644 --- a/sphinx/websupport/search/whooshsearch.py +++ b/sphinx/websupport/search/whooshsearch.py @@ -38,8 +38,8 @@ class WhooshSearch(BaseSearch): def finish_indexing(self): self.writer.commit() - def add_document(self, path, title, text): - self.writer.add_document(path=unicode(path), + def add_document(self, pagename, title, text): + self.writer.add_document(path=unicode(pagename), title=title, text=text) @@ -47,10 +47,10 @@ class WhooshSearch(BaseSearch): res = self.searcher.find('text', q) results = [] for result in res: - context = self.extract_context(result['text'], q) + context = self.extract_context(result['text']) results.append((result['path'], result.get('title', ''), context)) - return results, len(res), res.scored_length() + return results diff --git a/sphinx/websupport/search/xapiansearch.py b/sphinx/websupport/search/xapiansearch.py index f8dbecd9..f5ad9688 100644 --- a/sphinx/websupport/search/xapiansearch.py +++ b/sphinx/websupport/search/xapiansearch.py @@ -70,15 +70,13 @@ class XapianSearch(BaseSearch): # Find the top 100 results for the query. enquire.set_query(query) matches = enquire.get_mset(0, 100) - results_found = matches.get_matches_estimated() - results_displayed = matches.size() results = [] for m in matches: - context = self.extract_context(m.document.get_data(), q) + context = self.extract_context(m.document.get_data()) results.append((m.document.get_value(self.DOC_PATH), m.document.get_value(self.DOC_TITLE), ''.join(context) )) - return results, results_found, results_displayed + return results -- cgit v1.2.1 From 14b746ab635818fe4ef58f0ccfae99c58f890217 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Tue, 13 Jul 2010 17:00:23 -0500 Subject: small cleanup of xapiansearch.py --- sphinx/websupport/search/__init__.py | 4 ++-- sphinx/websupport/search/whooshsearch.py | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/sphinx/websupport/search/__init__.py b/sphinx/websupport/search/__init__.py index 1886776a..e1d7ea47 100644 --- a/sphinx/websupport/search/__init__.py +++ b/sphinx/websupport/search/__init__.py @@ -103,11 +103,11 @@ class BaseSearch(object): if res is None: return '' context_start = max(res.start() - length/2, 0) - context_end = start + length + context_end = context_start + length context = ''.join(['...' if context_start > 0 else '', text[context_start:context_end], '...' if context_end < len(text) else '']) - + try: return unicode(context, errors='ignore') except TypeError: diff --git a/sphinx/websupport/search/whooshsearch.py b/sphinx/websupport/search/whooshsearch.py index 00c7403c..52f49d8d 100644 --- a/sphinx/websupport/search/whooshsearch.py +++ b/sphinx/websupport/search/whooshsearch.py @@ -18,6 +18,9 @@ from sphinx.util.osutil import ensuredir from sphinx.websupport.search import BaseSearch class WhooshSearch(BaseSearch): + """The whoosh search adapter for sphinx web support.""" + + # Define the Whoosh Schema for the search index. schema = Schema(path=ID(stored=True, unique=True), title=TEXT(field_boost=2.0, stored=True), text=TEXT(analyzer=StemmingAnalyzer(), stored=True)) @@ -33,24 +36,22 @@ class WhooshSearch(BaseSearch): def init_indexing(self, changed=[]): for changed_path in changed: self.index.delete_by_term('path', changed_path) - self.writer = self.index.writer() + self.index_writer = self.index.writer() def finish_indexing(self): - self.writer.commit() + self.index_writer.commit() def add_document(self, pagename, title, text): - self.writer.add_document(path=unicode(pagename), - title=title, - text=text) + self.index_writer.add_document(path=unicode(pagename), + title=title, + text=text) def handle_query(self, q): - res = self.searcher.find('text', q) + whoosh_results = self.searcher.find('text', q) results = [] - for result in res: + for result in whoosh_results: context = self.extract_context(result['text']) - results.append((result['path'], result.get('title', ''), context)) - return results -- cgit v1.2.1 From a533a1a5d4ad14c61385a365bb19d62bf8000c44 Mon Sep 17 00:00:00 2001 From: Robert Lehmann <mail@robertlehmann.de> Date: Wed, 14 Jul 2010 22:01:08 +0200 Subject: Move translation patching back into transform for chronological order. --- sphinx/environment.py | 68 ++++++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/sphinx/environment.py b/sphinx/environment.py index ccdf7d15..d85ca27f 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -16,6 +16,7 @@ import types import codecs import imghdr import string +import posixpath import cPickle as pickle from os import path from glob import glob @@ -181,12 +182,48 @@ class CitationReferences(Transform): refnode += nodes.Text('[' + cittext + ']') citnode.parent.replace(citnode, refnode) +class Locale(Transform): + """ + Replace translatable nodes with their translated doctree. + """ + default_priority = 0 + def apply(self): + env = self.document.settings.env + settings, source = self.document.settings, self.document['source'] + # XXX check if this is reliable + docname = posixpath.splitext(posixpath.basename(source))[0] + section = docname.split(SEP, 1)[0] + + # fetch translations + dirs = [path.join(env.srcdir, x) + for x in env.config.locale_dirs] + catalog, empty = init_locale(dirs, env.config.language, section) + if not empty: + return + + parser = RSTParser() + + for node, msg in extract_messages(self.document): + ctx = node.parent + patch = new_document(source, settings) + msgstr = catalog.ugettext(msg) + #XXX add marker to untranslated parts + if not msgstr or msgstr == msg: # as-of-yet untranslated + continue + parser.parse(msgstr, patch) + patch = patch[0] + assert isinstance(patch, nodes.paragraph) + for child in patch.children: # update leaves + child.parent = node + node.children = patch.children + + class SphinxStandaloneReader(standalone.Reader): """ Add our own transforms. """ - transforms = [CitationReferences, DefaultSubstitutions, + transforms = [Locale, CitationReferences, DefaultSubstitutions, MoveModuleTargets, HandleCodeBlocks, SortIds] def get_transforms(self): @@ -595,7 +632,6 @@ class BuildEnvironment: Parse a file and add/update inventory entries for the doctree. If srcpath is given, read from a different source file. """ - section = docname.split(SEP, 1)[0] # remove all inventory entries for that file if app: app.emit('env-purge-doc', self, docname) @@ -660,7 +696,6 @@ class BuildEnvironment: # post-processing self.filter_messages(doctree) - self.process_translations(doctree, self.get_translation(section)) self.process_dependencies(docname, doctree) self.process_images(docname, doctree) self.process_downloads(docname, doctree) @@ -735,14 +770,6 @@ class BuildEnvironment: def note_dependency(self, filename): self.dependencies.setdefault(self.docname, set()).add(filename) - def get_translation(self, section): - dirs = [path.join(self.srcdir, x) for x in self.config.locale_dirs] - translation, has_trans = init_locale(dirs, - self.config.language, section) - if not has_trans: - return None - return translation - # post-processing of read doctrees def filter_messages(self, doctree): @@ -754,25 +781,6 @@ class BuildEnvironment: if node['level'] < filterlevel: node.parent.remove(node) - def process_translations(self, doctree, translation): - """ - Replace translatable nodes with their translated doctree. - """ - if not translation: - return - settings, source = doctree.settings, doctree['source'] - parser = RSTParser() - for node, msg in extract_messages(doctree): - ctx = node.parent - patch = new_document(source, settings) - msgstr = translation.ugettext(msg) - #XXX add marker to untranslated parts - if not msgstr or msgstr == msg: # as-of-yet untranslated - continue - parser.parse(msgstr, patch) - assert isinstance(patch[0], nodes.paragraph) - node.children = patch[0].children - def process_dependencies(self, docname, doctree): """ -- cgit v1.2.1 From c8f16b61cc6682fbbe47a5902b3c92e4b9336be0 Mon Sep 17 00:00:00 2001 From: Robert Lehmann <mail@robertlehmann.de> Date: Wed, 14 Jul 2010 23:41:38 +0200 Subject: Fixed docname resolution. --- sphinx/environment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sphinx/environment.py b/sphinx/environment.py index d85ca27f..b03e4625 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -191,7 +191,8 @@ class Locale(Transform): env = self.document.settings.env settings, source = self.document.settings, self.document['source'] # XXX check if this is reliable - docname = posixpath.splitext(posixpath.basename(source))[0] + assert source.startswith(env.srcdir) + docname = posixpath.splitext(source[len(env.srcdir):].lstrip('/'))[0] section = docname.split(SEP, 1)[0] # fetch translations -- cgit v1.2.1 From 99d260bd65a8664e18503385c6c9780797d97323 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Thu, 15 Jul 2010 13:25:12 -0500 Subject: separate sqlalchemystorage from __init__ --- doc/web/searchadapters.rst | 47 +++++++++ doc/web/storagebackends.rst | 47 +++++++++ doc/websupport.rst | 3 +- sphinx/websupport/__init__.py | 14 +-- sphinx/websupport/comments/__init__.py | 123 +---------------------- sphinx/websupport/comments/sqlalchemystorage.py | 125 ++++++++++++++++++++++++ 6 files changed, 229 insertions(+), 130 deletions(-) create mode 100644 doc/web/searchadapters.rst create mode 100644 doc/web/storagebackends.rst create mode 100644 sphinx/websupport/comments/sqlalchemystorage.py diff --git a/doc/web/searchadapters.rst b/doc/web/searchadapters.rst new file mode 100644 index 00000000..83e928ba --- /dev/null +++ b/doc/web/searchadapters.rst @@ -0,0 +1,47 @@ +.. _searchadapters: + +.. currentmodule:: sphinx.websupport.search + +Search Adapters +=============== + +To create a custom search adapter you will need to subclass the +:class:`~BaseSearch` class. Then create an instance of the new class +and pass that as the `search` keyword argument when you create the +:class:`~sphinx.websupport.WebSupport` object:: + + support = Websupport(srcdir=srcdir, + outdir=outdir, + search=MySearch()) + +For more information about creating a custom search adapter, please see +the documentation of the :class:`BaseSearch` class below. + +.. class:: BaseSearch + + Defines an interface for search adapters. + +BaseSearch Methods +~~~~~~~~~~~~~~~~~~ + + The following methods are defined in the BaseSearch class. Some methods + do not need to be overridden, but some ( + :meth:`~sphinx.websupport.search.BaseSearch.add_document` and + :meth:`~sphinx.websupport.search.BaseSearch.handle_query`) must be + overridden in your subclass. For a working example, look at the + built-in adapter for whoosh. + +.. automethod:: sphinx.websupport.search.BaseSearch.init_indexing + +.. automethod:: sphinx.websupport.search.BaseSearch.finish_indexing + +.. automethod:: sphinx.websupport.search.BaseSearch.feed + +.. automethod:: sphinx.websupport.search.BaseSearch.add_document + +.. automethod:: sphinx.websupport.search.BaseSearch.query + +.. automethod:: sphinx.websupport.search.BaseSearch.handle_query + +.. automethod:: sphinx.websupport.search.BaseSearch.extract_context + diff --git a/doc/web/storagebackends.rst b/doc/web/storagebackends.rst new file mode 100644 index 00000000..83e928ba --- /dev/null +++ b/doc/web/storagebackends.rst @@ -0,0 +1,47 @@ +.. _searchadapters: + +.. currentmodule:: sphinx.websupport.search + +Search Adapters +=============== + +To create a custom search adapter you will need to subclass the +:class:`~BaseSearch` class. Then create an instance of the new class +and pass that as the `search` keyword argument when you create the +:class:`~sphinx.websupport.WebSupport` object:: + + support = Websupport(srcdir=srcdir, + outdir=outdir, + search=MySearch()) + +For more information about creating a custom search adapter, please see +the documentation of the :class:`BaseSearch` class below. + +.. class:: BaseSearch + + Defines an interface for search adapters. + +BaseSearch Methods +~~~~~~~~~~~~~~~~~~ + + The following methods are defined in the BaseSearch class. Some methods + do not need to be overridden, but some ( + :meth:`~sphinx.websupport.search.BaseSearch.add_document` and + :meth:`~sphinx.websupport.search.BaseSearch.handle_query`) must be + overridden in your subclass. For a working example, look at the + built-in adapter for whoosh. + +.. automethod:: sphinx.websupport.search.BaseSearch.init_indexing + +.. automethod:: sphinx.websupport.search.BaseSearch.finish_indexing + +.. automethod:: sphinx.websupport.search.BaseSearch.feed + +.. automethod:: sphinx.websupport.search.BaseSearch.add_document + +.. automethod:: sphinx.websupport.search.BaseSearch.query + +.. automethod:: sphinx.websupport.search.BaseSearch.handle_query + +.. automethod:: sphinx.websupport.search.BaseSearch.extract_context + diff --git a/doc/websupport.rst b/doc/websupport.rst index 1b6725df..c7833e7a 100644 --- a/doc/websupport.rst +++ b/doc/websupport.rst @@ -12,4 +12,5 @@ into your web application. To learn more read the web/quickstart web/api web/frontend - web/searchadapters \ No newline at end of file + web/searchadapters + web/storagebackends \ No newline at end of file diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 407cb4c9..c60c9350 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -18,7 +18,7 @@ from jinja2 import Environment, FileSystemLoader from sphinx.application import Sphinx from sphinx.util.osutil import ensuredir from sphinx.websupport.search import BaseSearch, search_adapters -from sphinx.websupport import comments as sphinxcomments +from sphinx.websupport.comments import StorageBackend class WebSupportApp(Sphinx): def __init__(self, *args, **kwargs): @@ -46,17 +46,18 @@ class WebSupport(object): self._init_comments(comments) def _init_comments(self, comments): - if isinstance(comments, sphinxcomments.CommentBackend): + if isinstance(comments, StorageBackend): self.comments = comments else: - # If a CommentBackend isn't provided, use the default + # If a StorageBackend isn't provided, use the default # SQLAlchemy backend with an SQLite db. - from sphinx.websupport.comments import SQLAlchemyComments + from sphinx.websupport.comments.sqlalchemystorage \ + import SQLAlchemyStorage from sqlalchemy import create_engine db_path = path.join(self.outdir, 'comments', 'comments.db') ensuredir(path.dirname(db_path)) engine = create_engine('sqlite:///%s' % db_path) - self.comments = SQLAlchemyComments(engine) + self.comments = SQLAlchemyStorage(engine) def _init_templating(self): import sphinx @@ -93,8 +94,7 @@ class WebSupport(object): self.outdir, doctreedir, 'websupport', search=self.search, comments=self.comments) - # TODO: - # Hook comments into Sphinx signals. + self.comments.pre_build() app.build() self.comments.post_build() diff --git a/sphinx/websupport/comments/__init__.py b/sphinx/websupport/comments/__init__.py index 395bde58..66b9012a 100644 --- a/sphinx/websupport/comments/__init__.py +++ b/sphinx/websupport/comments/__init__.py @@ -1,12 +1,5 @@ -from datetime import datetime -from sqlalchemy.orm import sessionmaker - -from sphinx.websupport.comments.db import Base, Node, Comment, Vote - -Session = sessionmaker() - -class CommentBackend(object): +class StorageBackend(object): def pre_build(self): pass @@ -22,117 +15,3 @@ class CommentBackend(object): def get_comments(self, parent_id): raise NotImplemented - - -class SQLAlchemyComments(CommentBackend): - def __init__(self, engine): - self.engine = engine - Base.metadata.bind = engine - Base.metadata.create_all() - Session.configure(bind=engine) - self.session = Session() - - def pre_build(self): - self.current_pk = None - - def add_node(self, document, line, source, treeloc): - node = Node(document, line, source, treeloc) - self.session.add(node) - if self.current_pk is None: - self.session.commit() - self.current_pk = node.id - else: - self.current_pk += 1 - return self.current_pk - - def post_build(self): - self.session.commit() - - def add_comment(self, parent_id, text, displayed, - username, rating, time): - time = time or datetime.now() - - id = parent_id[1:] - if parent_id[0] == 's': - node = self.session.query(Node).filter(Node.id == id).first() - comment = Comment(text, displayed, username, rating, - time, node=node) - elif parent_id[0] == 'c': - parent = self.session.query(Comment).filter(Comment.id == id).first() - comment = Comment(text, displayed, username, rating, - time, parent=parent) - - self.session.add(comment) - self.session.commit() - return self.serializable(comment) - - def get_comments(self, parent_id, user_id): - parent_id = parent_id[1:] - node = self.session.query(Node).filter(Node.id == parent_id).first() - comments = [] - for comment in node.comments: - comments.append(self.serializable(comment, user_id)) - - return comments - - def process_vote(self, comment_id, user_id, value): - vote = self.session.query(Vote).filter( - Vote.comment_id == comment_id).filter( - Vote.user_id == user_id).first() - - comment = self.session.query(Comment).filter( - Comment.id == comment_id).first() - - if vote is None: - vote = Vote(comment_id, user_id, value) - comment.rating += value - else: - comment.rating += value - vote.value - vote.value = value - self.session.add(vote) - self.session.commit() - - def serializable(self, comment, user_id=None): - delta = datetime.now() - comment.time - - time = {'year': comment.time.year, - 'month': comment.time.month, - 'day': comment.time.day, - 'hour': comment.time.hour, - 'minute': comment.time.minute, - 'second': comment.time.second, - 'iso': comment.time.isoformat(), - 'delta': self.pretty_delta(delta)} - - vote = '' - if user_id is not None: - vote = self.session.query(Vote).filter( - Vote.comment_id == comment.id).filter( - Vote.user_id == user_id).first() - if vote is not None: - vote = vote.value - - return {'text': comment.text, - 'username': comment.username or 'Anonymous', - 'id': comment.id, - 'rating': comment.rating, - 'age': delta.seconds, - 'time': time, - 'vote': vote or 0, - 'node': comment.node.id if comment.node else None, - 'parent': comment.parent.id if comment.parent else None, - 'children': [self.serializable(child, user_id) - for child in comment.children]} - - def pretty_delta(self, delta): - days = delta.days - seconds = delta.seconds - hours = seconds / 3600 - minutes = seconds / 60 - - if days == 0: - dt = (minutes, 'minute') if hours == 0 else (hours, 'hour') - else: - dt = (days, 'day') - - return '%s %s ago' % dt if dt[0] == 1 else '%s %ss ago' % dt diff --git a/sphinx/websupport/comments/sqlalchemystorage.py b/sphinx/websupport/comments/sqlalchemystorage.py new file mode 100644 index 00000000..31403c0a --- /dev/null +++ b/sphinx/websupport/comments/sqlalchemystorage.py @@ -0,0 +1,125 @@ +from datetime import datetime + +from sqlalchemy.orm import sessionmaker + +from sphinx.websupport.comments import StorageBackend +from sphinx.websupport.comments.db import Base, Node, Comment, Vote + +Session = sessionmaker() + +class SQLAlchemyStorage(StorageBackend): + def __init__(self, engine): + self.engine = engine + Base.metadata.bind = engine + Base.metadata.create_all() + Session.configure(bind=engine) + + def pre_build(self): + self.build_session = Session() + + def add_node(self, document, line, source, treeloc): + node = Node(document, line, source, treeloc) + self.build_session.add(node) + self.build_session.flush() + return node.id + + def post_build(self): + self.build_session.commit() + self.build_session.close() + + def add_comment(self, parent_id, text, displayed, + username, rating, time): + time = time or datetime.now() + + session = Session() + + id = parent_id[1:] + if parent_id[0] == 's': + node = session.query(Node).filter(Node.id == id).first() + comment = Comment(text, displayed, username, rating, + time, node=node) + elif parent_id[0] == 'c': + parent = session.query(Comment).filter(Comment.id == id).first() + comment = Comment(text, displayed, username, rating, + time, parent=parent) + + session.add(comment) + session.commit() + comment = self.serializable(session, comment) + session.close() + return comment + + def get_comments(self, parent_id, user_id): + parent_id = parent_id[1:] + session = Session() + node = session.query(Node).filter(Node.id == parent_id).first() + comments = [] + for comment in node.comments: + comments.append(self.serializable(session, comment, user_id)) + + session.close() + return comments + + def process_vote(self, comment_id, user_id, value): + session = Session() + vote = session.query(Vote).filter( + Vote.comment_id == comment_id).filter( + Vote.user_id == user_id).first() + + comment = session.query(Comment).filter( + Comment.id == comment_id).first() + + if vote is None: + vote = Vote(comment_id, user_id, value) + comment.rating += value + else: + comment.rating += value - vote.value + vote.value = value + session.add(vote) + session.commit() + session.close() + + def serializable(self, session, comment, user_id=None): + delta = datetime.now() - comment.time + + time = {'year': comment.time.year, + 'month': comment.time.month, + 'day': comment.time.day, + 'hour': comment.time.hour, + 'minute': comment.time.minute, + 'second': comment.time.second, + 'iso': comment.time.isoformat(), + 'delta': self.pretty_delta(delta)} + + vote = '' + if user_id is not None: + vote = session.query(Vote).filter( + Vote.comment_id == comment.id).filter( + Vote.user_id == user_id).first() + if vote is not None: + vote = vote.value + + return {'text': comment.text, + 'username': comment.username or 'Anonymous', + 'id': comment.id, + 'rating': comment.rating, + 'age': delta.seconds, + 'time': time, + 'vote': vote or 0, + 'node': comment.node.id if comment.node else None, + 'parent': comment.parent.id if comment.parent else None, + 'children': [self.serializable(session, child, user_id) + for child in comment.children]} + + def pretty_delta(self, delta): + days = delta.days + seconds = delta.seconds + hours = seconds / 3600 + minutes = seconds / 60 + + if days == 0: + dt = (minutes, 'minute') if hours == 0 else (hours, 'hour') + else: + dt = (days, 'day') + + return '%s %s ago' % dt if dt[0] == 1 else '%s %ss ago' % dt -- cgit v1.2.1 From fc759d41327188a61b2c7e001e9b0a944a4bbd3a Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Fri, 16 Jul 2010 13:31:16 -0500 Subject: More docs --- doc/web/storagebackends.rst | 50 ++++++------------------- sphinx/websupport/comments/__init__.py | 18 +++++++++ sphinx/websupport/comments/sqlalchemystorage.py | 2 + 3 files changed, 31 insertions(+), 39 deletions(-) diff --git a/doc/web/storagebackends.rst b/doc/web/storagebackends.rst index 83e928ba..87e1b478 100644 --- a/doc/web/storagebackends.rst +++ b/doc/web/storagebackends.rst @@ -1,47 +1,19 @@ -.. _searchadapters: +.. _storagebackends: -.. currentmodule:: sphinx.websupport.search +.. currentmodule:: sphinx.websupport.comments -Search Adapters -=============== +Storage Backends +================ -To create a custom search adapter you will need to subclass the -:class:`~BaseSearch` class. Then create an instance of the new class -and pass that as the `search` keyword argument when you create the -:class:`~sphinx.websupport.WebSupport` object:: +StorageBackend Methods +~~~~~~~~~~~~~~~~~~~~~~ - support = Websupport(srcdir=srcdir, - outdir=outdir, - search=MySearch()) +.. automethod:: sphinx.websupport.comments.StorageBackend.pre_build -For more information about creating a custom search adapter, please see -the documentation of the :class:`BaseSearch` class below. +.. automethod:: sphinx.websupport.comments.StorageBackend.add_node -.. class:: BaseSearch +.. automethod:: sphinx.websupport.comments.StorageBackend.post_build - Defines an interface for search adapters. - -BaseSearch Methods -~~~~~~~~~~~~~~~~~~ - - The following methods are defined in the BaseSearch class. Some methods - do not need to be overridden, but some ( - :meth:`~sphinx.websupport.search.BaseSearch.add_document` and - :meth:`~sphinx.websupport.search.BaseSearch.handle_query`) must be - overridden in your subclass. For a working example, look at the - built-in adapter for whoosh. - -.. automethod:: sphinx.websupport.search.BaseSearch.init_indexing - -.. automethod:: sphinx.websupport.search.BaseSearch.finish_indexing - -.. automethod:: sphinx.websupport.search.BaseSearch.feed - -.. automethod:: sphinx.websupport.search.BaseSearch.add_document - -.. automethod:: sphinx.websupport.search.BaseSearch.query - -.. automethod:: sphinx.websupport.search.BaseSearch.handle_query - -.. automethod:: sphinx.websupport.search.BaseSearch.extract_context +.. automethod:: sphinx.websupport.comments.StorageBackend.add_comment +.. automethod:: sphinx.websupport.comments.StorageBackend.get_comments diff --git a/sphinx/websupport/comments/__init__.py b/sphinx/websupport/comments/__init__.py index 66b9012a..20d92386 100644 --- a/sphinx/websupport/comments/__init__.py +++ b/sphinx/websupport/comments/__init__.py @@ -1,17 +1,35 @@ class StorageBackend(object): def pre_build(self): + """Called immediately before the build process begins. Use this + to prepare the StorageBackend for the addition of nodes. + """ pass def add_node(self, document, line, source, treeloc): + """Add a node to the StorageBackend. + + `document` is the name of the document the node belongs to. + + `line` is the line in the source where the node begins. + + `source` is the source files name. + + `treeloc` is for future use. + """ raise NotImplemented def post_build(self): + """Called after a build has completed. Use this to finalize the + addition of nodes if needed. + """ pass def add_comment(self, parent_id, text, displayed, username, rating, time): + """Called when a comment is being added.""" raise NotImplemented def get_comments(self, parent_id): + """Called to retrieve all comments for a node.""" raise NotImplemented diff --git a/sphinx/websupport/comments/sqlalchemystorage.py b/sphinx/websupport/comments/sqlalchemystorage.py index 31403c0a..3d4672bf 100644 --- a/sphinx/websupport/comments/sqlalchemystorage.py +++ b/sphinx/websupport/comments/sqlalchemystorage.py @@ -33,6 +33,8 @@ class SQLAlchemyStorage(StorageBackend): session = Session() + + id = parent_id[1:] if parent_id[0] == 's': node = session.query(Node).filter(Node.id == id).first() -- cgit v1.2.1 From ee27c3763773edf6cfbbe6bacef1a11238c3df28 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 19 Jul 2010 11:37:17 -0500 Subject: remove whitespace --- sphinx/websupport/comments/sqlalchemystorage.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sphinx/websupport/comments/sqlalchemystorage.py b/sphinx/websupport/comments/sqlalchemystorage.py index 3d4672bf..a14ef476 100644 --- a/sphinx/websupport/comments/sqlalchemystorage.py +++ b/sphinx/websupport/comments/sqlalchemystorage.py @@ -32,8 +32,6 @@ class SQLAlchemyStorage(StorageBackend): time = time or datetime.now() session = Session() - - id = parent_id[1:] if parent_id[0] == 's': -- cgit v1.2.1 From 1bc72fb7c52e5f22bf789f9f9aaf3e1750c2cc6a Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 19 Jul 2010 14:12:15 -0500 Subject: started proposal backend --- sphinx/websupport/__init__.py | 36 ++++++++++++++--------- sphinx/websupport/comments/__init__.py | 16 ++++++++-- sphinx/websupport/comments/db.py | 23 ++++++++++++++- sphinx/websupport/comments/sqlalchemystorage.py | 39 ++++++++++++++++++++----- sphinx/writers/websupport.py | 10 +++---- 5 files changed, 93 insertions(+), 31 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index c60c9350..7c39681a 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -23,7 +23,7 @@ from sphinx.websupport.comments import StorageBackend class WebSupportApp(Sphinx): def __init__(self, *args, **kwargs): self.search = kwargs.pop('search', None) - self.comments = kwargs.pop('comments', None) + self.storage = kwargs.pop('storage', None) Sphinx.__init__(self, *args, **kwargs) class WebSupport(object): @@ -32,7 +32,7 @@ class WebSupport(object): """ def __init__(self, srcdir='', outdir='', datadir='', search=None, - comments=None): + storage=None): self.srcdir = srcdir self.outdir = outdir or path.join(self.srcdir, '_build', 'websupport') @@ -43,21 +43,21 @@ class WebSupport(object): if search is not None: self._init_search(search) - self._init_comments(comments) + self._init_storage(storage) - def _init_comments(self, comments): - if isinstance(comments, StorageBackend): - self.comments = comments + def _init_storage(self, storage): + if isinstance(storage, StorageBackend): + self.storage = storage else: # If a StorageBackend isn't provided, use the default # SQLAlchemy backend with an SQLite db. from sphinx.websupport.comments.sqlalchemystorage \ import SQLAlchemyStorage from sqlalchemy import create_engine - db_path = path.join(self.outdir, 'comments', 'comments.db') + db_path = path.join(self.outdir, 'db', 'websupport.db') ensuredir(path.dirname(db_path)) engine = create_engine('sqlite:///%s' % db_path) - self.comments = SQLAlchemyStorage(engine) + self.storage = SQLAlchemyStorage(engine) def _init_templating(self): import sphinx @@ -93,11 +93,11 @@ class WebSupport(object): app = WebSupportApp(self.srcdir, self.srcdir, self.outdir, doctreedir, 'websupport', search=self.search, - comments=self.comments) + storage=self.storage) - self.comments.pre_build() + self.storage.pre_build() app.build() - self.comments.post_build() + self.storage.post_build() def get_document(self, docname): """Load and return a document from a pickle. The document will @@ -178,7 +178,7 @@ class WebSupport(object): :param node_id: the id of the node to get comments for. :param user_id: the id of the user viewing the comments. """ - return self.comments.get_comments(node_id, user_id) + return self.storage.get_comments(node_id, user_id) def add_comment(self, parent_id, text, displayed=True, username=None, rating=0, time=None): @@ -202,9 +202,17 @@ class WebSupport(object): :param rating: the starting rating of the comment, defaults to 0. :param time: the time the comment was created, defaults to now. """ - return self.comments.add_comment(parent_id, text, displayed, + return self.storage.add_comment(parent_id, text, displayed, username, rating, time) + def get_proposals(self, node_id, user_id=None): + return self.storage.get_proposals(node_id, user_id) + + def add_proposal(self, parent_id, text, displayed=True, username=None, + rating=0, time=None): + return self.storage.add_proposal(parent_id, text, displayed, + username, rating, time) + def process_vote(self, comment_id, user_id, value): """Process a user's vote. The web support package relies on the API user to perform authentication. The API user will @@ -230,4 +238,4 @@ class WebSupport(object): :param value: 1 for an upvote, -1 for a downvote, 0 for an unvote. """ value = int(value) - self.comments.process_vote(comment_id, user_id, value) + self.storage.process_vote(comment_id, user_id, value) diff --git a/sphinx/websupport/comments/__init__.py b/sphinx/websupport/comments/__init__.py index 20d92386..a78a19c1 100644 --- a/sphinx/websupport/comments/__init__.py +++ b/sphinx/websupport/comments/__init__.py @@ -17,7 +17,7 @@ class StorageBackend(object): `treeloc` is for future use. """ - raise NotImplemented + raise NotImplementedError() def post_build(self): """Called after a build has completed. Use this to finalize the @@ -28,8 +28,18 @@ class StorageBackend(object): def add_comment(self, parent_id, text, displayed, username, rating, time): """Called when a comment is being added.""" - raise NotImplemented + raise NotImplementedError() def get_comments(self, parent_id): """Called to retrieve all comments for a node.""" - raise NotImplemented + raise NotImplementedError() + + def add_proposal(self, parent_id, text, displayed, username, + rating, time): + raise NotImplementedError() + + def get_proposals(self, parent_id): + raise NotImplementedError() + + def process_vote(self, comment_id, user_id, value): + raise NotImplementedError() diff --git a/sphinx/websupport/comments/db.py b/sphinx/websupport/comments/db.py index 22020b4b..1b3cd9d8 100644 --- a/sphinx/websupport/comments/db.py +++ b/sphinx/websupport/comments/db.py @@ -50,7 +50,7 @@ class Comment(Base): self.node = node self.parent = parent -class Vote(Base): +class CommentVote(Base): __tablename__ = db_prefix + 'vote' id = Column(Integer, primary_key=True) @@ -68,4 +68,25 @@ class Vote(Base): self.value = value self.user_id = user_id self.comment_id = comment_id + +class Proposal(Base): + __tablename__ = db_prefix + 'proposals' + + id = Column(Integer, primary_key=True) + rating = Column(Integer, nullable=False) + time = Column(DateTime, nullable=False) + text = Column(Text, nullable=False) + displayed = Column(Boolean, index=True, default=False) + username = Column(String(64)) + + node_id = Column(Integer, ForeignKey(db_prefix + 'nodes.id')) + node = relation(Node, backref='proposals') + + def __init__(self, text, displayed, username, rating, time, node): + self.text = text + self.displayed = displayed + self.username = username + self.rating = rating + self.time = time + self.node = node diff --git a/sphinx/websupport/comments/sqlalchemystorage.py b/sphinx/websupport/comments/sqlalchemystorage.py index a14ef476..26f352ac 100644 --- a/sphinx/websupport/comments/sqlalchemystorage.py +++ b/sphinx/websupport/comments/sqlalchemystorage.py @@ -3,7 +3,7 @@ from datetime import datetime from sqlalchemy.orm import sessionmaker from sphinx.websupport.comments import StorageBackend -from sphinx.websupport.comments.db import Base, Node, Comment, Vote +from sphinx.websupport.comments.db import Base, Node, Comment, CommentVote Session = sessionmaker() @@ -60,17 +60,40 @@ class SQLAlchemyStorage(StorageBackend): session.close() return comments + def add_proposal(self, parent_id, text, displayed, username, + rating, time): + time = time or datetime.now() + + session = Session() + + node = session.query(Node).filter(Node.id == parent_id).first() + proposal= Proposal(text, displayed, username, rating, time, node) + + session.add(proposal) + session.commit() + session.close() + return proposal + + def get_proposals(self, parent_id): + session = Session() + node = session.query(Node).filter(Node.id == parent_id).first() + proposals = [] + + # TODO + + return proposals + def process_vote(self, comment_id, user_id, value): session = Session() - vote = session.query(Vote).filter( - Vote.comment_id == comment_id).filter( - Vote.user_id == user_id).first() + vote = session.query(CommentVote).filter( + CommentVote.comment_id == comment_id).filter( + CommentVote.user_id == user_id).first() comment = session.query(Comment).filter( Comment.id == comment_id).first() if vote is None: - vote = Vote(comment_id, user_id, value) + vote = CommentVote(comment_id, user_id, value) comment.rating += value else: comment.rating += value - vote.value @@ -93,9 +116,9 @@ class SQLAlchemyStorage(StorageBackend): vote = '' if user_id is not None: - vote = session.query(Vote).filter( - Vote.comment_id == comment.id).filter( - Vote.user_id == user_id).first() + vote = session.query(CommentVote).filter( + CommentVote.comment_id == comment.id).filter( + CommentVote.user_id == user_id).first() if vote is not None: vote = vote.value diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index 18c0807d..04c989b2 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -57,9 +57,9 @@ class WebSupportTranslator(HTMLTranslator): self.in_commentable = False def add_db_node(self, node): - comments = self.builder.app.comments - db_node_id = comments.add_node(document=self.builder.cur_docname, - line=node.line, - source=node.rawsource, - treeloc='???') + storage = self.builder.app.storage + db_node_id = storage.add_node(document=self.builder.cur_docname, + line=node.line, + source=node.rawsource, + treeloc='???') return db_node_id -- cgit v1.2.1 From a06e5ef5970301e01a3d6fa40a52bf1118a28f90 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 19 Jul 2010 14:53:37 -0500 Subject: moved serializable() to db.Comment --- sphinx/websupport/comments/db.py | 54 ++++++++++++++++++++++++- sphinx/websupport/comments/sqlalchemystorage.py | 54 ++----------------------- 2 files changed, 55 insertions(+), 53 deletions(-) diff --git a/sphinx/websupport/comments/db.py b/sphinx/websupport/comments/db.py index 1b3cd9d8..9d0fc2a2 100644 --- a/sphinx/websupport/comments/db.py +++ b/sphinx/websupport/comments/db.py @@ -1,11 +1,15 @@ +from datetime import datetime + from sqlalchemy import Column, Integer, Text, String, Boolean, ForeignKey,\ DateTime from sqlalchemy.schema import UniqueConstraint from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relation +from sqlalchemy.orm import relation, sessionmaker Base = declarative_base() +Session = sessionmaker() + db_prefix = 'sphinx_' class Node(Base): @@ -50,8 +54,54 @@ class Comment(Base): self.node = node self.parent = parent + def serializable(self, user_id=None): + delta = datetime.now() - self.time + + time = {'year': self.time.year, + 'month': self.time.month, + 'day': self.time.day, + 'hour': self.time.hour, + 'minute': self.time.minute, + 'second': self.time.second, + 'iso': self.time.isoformat(), + 'delta': self.pretty_delta(delta)} + + vote = '' + if user_id is not None: + session = Session() + vote = session.query(CommentVote).filter( + CommentVote.comment_id == self.id).filter( + CommentVote.user_id == user_id).first() + vote = vote.value if vote is not None else 0 + session.close() + + return {'text': self.text, + 'username': self.username or 'Anonymous', + 'id': self.id, + 'rating': self.rating, + 'age': delta.seconds, + 'time': time, + 'vote': vote or 0, + 'node': self.node.id if self.node else None, + 'parent': self.parent.id if self.parent else None, + 'children': [child.serializable(user_id) + for child in self.children]} + + def pretty_delta(self, delta): + days = delta.days + seconds = delta.seconds + hours = seconds / 3600 + minutes = seconds / 60 + + if days == 0: + dt = (minutes, 'minute') if hours == 0 else (hours, 'hour') + else: + dt = (days, 'day') + + return '%s %s ago' % dt if dt[0] == 1 else '%s %ss ago' % dt + class CommentVote(Base): - __tablename__ = db_prefix + 'vote' + __tablename__ = db_prefix + 'commentvote' id = Column(Integer, primary_key=True) # -1 if downvoted, +1 if upvoted, 0 if voted then unvoted. diff --git a/sphinx/websupport/comments/sqlalchemystorage.py b/sphinx/websupport/comments/sqlalchemystorage.py index 26f352ac..10f1fa3c 100644 --- a/sphinx/websupport/comments/sqlalchemystorage.py +++ b/sphinx/websupport/comments/sqlalchemystorage.py @@ -1,11 +1,7 @@ from datetime import datetime -from sqlalchemy.orm import sessionmaker - from sphinx.websupport.comments import StorageBackend -from sphinx.websupport.comments.db import Base, Node, Comment, CommentVote - -Session = sessionmaker() +from sphinx.websupport.comments.db import Base, Node, Comment, CommentVote, Session class SQLAlchemyStorage(StorageBackend): def __init__(self, engine): @@ -45,7 +41,7 @@ class SQLAlchemyStorage(StorageBackend): session.add(comment) session.commit() - comment = self.serializable(session, comment) + comment = comment.serializable() session.close() return comment @@ -55,7 +51,7 @@ class SQLAlchemyStorage(StorageBackend): node = session.query(Node).filter(Node.id == parent_id).first() comments = [] for comment in node.comments: - comments.append(self.serializable(session, comment, user_id)) + comments.append(comment.serializable(user_id)) session.close() return comments @@ -102,47 +98,3 @@ class SQLAlchemyStorage(StorageBackend): session.commit() session.close() - def serializable(self, session, comment, user_id=None): - delta = datetime.now() - comment.time - - time = {'year': comment.time.year, - 'month': comment.time.month, - 'day': comment.time.day, - 'hour': comment.time.hour, - 'minute': comment.time.minute, - 'second': comment.time.second, - 'iso': comment.time.isoformat(), - 'delta': self.pretty_delta(delta)} - - vote = '' - if user_id is not None: - vote = session.query(CommentVote).filter( - CommentVote.comment_id == comment.id).filter( - CommentVote.user_id == user_id).first() - if vote is not None: - vote = vote.value - - return {'text': comment.text, - 'username': comment.username or 'Anonymous', - 'id': comment.id, - 'rating': comment.rating, - 'age': delta.seconds, - 'time': time, - 'vote': vote or 0, - 'node': comment.node.id if comment.node else None, - 'parent': comment.parent.id if comment.parent else None, - 'children': [self.serializable(session, child, user_id) - for child in comment.children]} - - def pretty_delta(self, delta): - days = delta.days - seconds = delta.seconds - hours = seconds / 3600 - minutes = seconds / 60 - - if days == 0: - dt = (minutes, 'minute') if hours == 0 else (hours, 'hour') - else: - dt = (days, 'day') - - return '%s %s ago' % dt if dt[0] == 1 else '%s %ss ago' % dt -- cgit v1.2.1 From f646ddb8936a58ce30f7705d99d4f69f4ddb1d4a Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 19 Jul 2010 14:58:06 -0500 Subject: use composite pk for db.CommentVote --- sphinx/websupport/comments/db.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sphinx/websupport/comments/db.py b/sphinx/websupport/comments/db.py index 9d0fc2a2..4cf8878f 100644 --- a/sphinx/websupport/comments/db.py +++ b/sphinx/websupport/comments/db.py @@ -103,13 +103,12 @@ class Comment(Base): class CommentVote(Base): __tablename__ = db_prefix + 'commentvote' - id = Column(Integer, primary_key=True) + user_id = Column(Integer, primary_key=True) # -1 if downvoted, +1 if upvoted, 0 if voted then unvoted. value = Column(Integer, nullable=False) - user_id = Column(Integer, index=True, nullable=False) comment_id = Column(Integer, ForeignKey(db_prefix + 'comments.id'), - nullable=False) + primary_key=True) comment = relation(Comment, backref="votes") __table_args__ = (UniqueConstraint(comment_id, user_id), {}) -- cgit v1.2.1 From bf4bffe0d6d8bbd46d8e0c257bc766132c467236 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 19 Jul 2010 15:14:02 -0500 Subject: add ProposalVote table to db --- sphinx/websupport/comments/db.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/sphinx/websupport/comments/db.py b/sphinx/websupport/comments/db.py index 4cf8878f..31f4ff77 100644 --- a/sphinx/websupport/comments/db.py +++ b/sphinx/websupport/comments/db.py @@ -111,8 +111,6 @@ class CommentVote(Base): primary_key=True) comment = relation(Comment, backref="votes") - __table_args__ = (UniqueConstraint(comment_id, user_id), {}) - def __init__(self, comment_id, user_id, value): self.value = value self.user_id = user_id @@ -139,3 +137,19 @@ class Proposal(Base): self.time = time self.node = node +class ProposalVote(Base): + __tablename__ = db_prefix + 'proposalvote' + + user_id = Column(Integer, primary_key=True) + # -1 if downvoted, +1 if upvoted, 0 if voted then unvoted. + value = Column(Integer, nullable=False) + + proposal_id = Column(Integer, ForeignKey(db_prefix + 'proposals.id'), + primary_key=True) + proposal = relation(Proposal, backref="votes") + + def __init__(self, proposal_id, user_id, value): + self.value = value + self.user_id = user_id + self.proposal_id = proposal_id + -- cgit v1.2.1 From 00f015710e26870f1516d9e72ecccd20133a62b9 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Tue, 20 Jul 2010 10:03:20 +0100 Subject: Fix undefined locals. --- sphinx/websupport/comments/db.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/sphinx/websupport/comments/db.py b/sphinx/websupport/comments/db.py index 31f4ff77..9ffe2fd8 100644 --- a/sphinx/websupport/comments/db.py +++ b/sphinx/websupport/comments/db.py @@ -12,16 +12,17 @@ Session = sessionmaker() db_prefix = 'sphinx_' + class Node(Base): """Data about a Node in a doctree.""" __tablename__ = db_prefix + 'nodes' - + id = Column(Integer, primary_key=True) document = Column(String(256), nullable=False) line = Column(Integer) source = Column(Text, nullable=False) treeloc = Column(String(32), nullable=False) - + def __init__(self, document, line, source, treeloc): self.document = document self.line = line @@ -44,7 +45,7 @@ class Comment(Base): parent_id = Column(Integer, ForeignKey(db_prefix + 'comments.id')) parent = relation('Comment', backref='children', remote_side=[id]) - def __init__(self, text, displayed, username, rating, time, + def __init__(self, text, displayed, username, rating, time, node=None, parent=None): self.text = text self.displayed = displayed @@ -84,7 +85,7 @@ class Comment(Base): 'vote': vote or 0, 'node': self.node.id if self.node else None, 'parent': self.parent.id if self.parent else None, - 'children': [child.serializable(user_id) + 'children': [child.serializable(user_id) for child in self.children]} def pretty_delta(self, delta): @@ -99,7 +100,7 @@ class Comment(Base): dt = (days, 'day') return '%s %s ago' % dt if dt[0] == 1 else '%s %ss ago' % dt - + class CommentVote(Base): __tablename__ = db_prefix + 'commentvote' @@ -136,7 +137,7 @@ class Proposal(Base): self.rating = rating self.time = time self.node = node - + class ProposalVote(Base): __tablename__ = db_prefix + 'proposalvote' -- cgit v1.2.1 From a799d4bf5831a52506474f16ffb5c6c4f7b562ac Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Tue, 20 Jul 2010 10:03:34 +0100 Subject: Style nits, trailing whitespace. --- sphinx/websupport/comments/__init__.py | 10 +++++----- sphinx/websupport/search/whooshsearch.py | 4 ++-- sphinx/websupport/search/xapiansearch.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sphinx/websupport/comments/__init__.py b/sphinx/websupport/comments/__init__.py index a78a19c1..47e7440c 100644 --- a/sphinx/websupport/comments/__init__.py +++ b/sphinx/websupport/comments/__init__.py @@ -10,22 +10,22 @@ class StorageBackend(object): """Add a node to the StorageBackend. `document` is the name of the document the node belongs to. - + `line` is the line in the source where the node begins. `source` is the source files name. - + `treeloc` is for future use. """ raise NotImplementedError() - + def post_build(self): """Called after a build has completed. Use this to finalize the addition of nodes if needed. """ pass - def add_comment(self, parent_id, text, displayed, username, + def add_comment(self, parent_id, text, displayed, username, rating, time): """Called when a comment is being added.""" raise NotImplementedError() @@ -34,7 +34,7 @@ class StorageBackend(object): """Called to retrieve all comments for a node.""" raise NotImplementedError() - def add_proposal(self, parent_id, text, displayed, username, + def add_proposal(self, parent_id, text, displayed, username, rating, time): raise NotImplementedError() diff --git a/sphinx/websupport/search/whooshsearch.py b/sphinx/websupport/search/whooshsearch.py index 52f49d8d..658b764d 100644 --- a/sphinx/websupport/search/whooshsearch.py +++ b/sphinx/websupport/search/whooshsearch.py @@ -40,10 +40,10 @@ class WhooshSearch(BaseSearch): def finish_indexing(self): self.index_writer.commit() - + def add_document(self, pagename, title, text): self.index_writer.add_document(path=unicode(pagename), - title=title, + title=title, text=text) def handle_query(self, q): diff --git a/sphinx/websupport/search/xapiansearch.py b/sphinx/websupport/search/xapiansearch.py index f5ad9688..16c7e2b1 100644 --- a/sphinx/websupport/search/xapiansearch.py +++ b/sphinx/websupport/search/xapiansearch.py @@ -28,7 +28,7 @@ class XapianSearch(BaseSearch): def init_indexing(self, changed=[]): ensuredir(self.db_path) - self.database = xapian.WritableDatabase(self.db_path, + self.database = xapian.WritableDatabase(self.db_path, xapian.DB_CREATE_OR_OPEN) self.indexer = xapian.TermGenerator() stemmer = xapian.Stem("english") @@ -37,7 +37,7 @@ class XapianSearch(BaseSearch): def finish_indexing(self): # Ensure the db lock is removed. del self.database - + def add_document(self, path, title, text): self.database.begin_transaction() # sphinx_page_path is used to easily retrieve documents by path. -- cgit v1.2.1 From c1ed5e2d7ba86a5ed35cb8761a235db4e9c6eff6 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Tue, 20 Jul 2010 10:13:43 +0100 Subject: Use try-finally for open file. --- sphinx/themes/basic/searchresults.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/themes/basic/searchresults.html b/sphinx/themes/basic/searchresults.html index e7fc84f8..4b5da1a3 100644 --- a/sphinx/themes/basic/searchresults.html +++ b/sphinx/themes/basic/searchresults.html @@ -20,7 +20,7 @@ {% if search_performed %} <h2>Search Results</h2> {% if not search_results %} -<p>'Your search did not match any results.</p> +<p>Your search did not match any results.</p> {% endif %} {% endif %} <div id="search-results"> -- cgit v1.2.1 From 984114e111fd02a58fbb9df3464d4d8d29d2e227 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Thu, 22 Jul 2010 16:42:29 -0500 Subject: Unification of Comments and Proposals. --- sphinx/websupport/comments/db.py | 42 +++------------------------------------- 1 file changed, 3 insertions(+), 39 deletions(-) diff --git a/sphinx/websupport/comments/db.py b/sphinx/websupport/comments/db.py index 31f4ff77..3a33cb26 100644 --- a/sphinx/websupport/comments/db.py +++ b/sphinx/websupport/comments/db.py @@ -37,6 +37,7 @@ class Comment(Base): text = Column(Text, nullable=False) displayed = Column(Boolean, index=True, default=False) username = Column(String(64)) + proposal = Column(Text) node_id = Column(Integer, ForeignKey(db_prefix + 'nodes.id')) node = relation(Node, backref='comments') @@ -45,7 +46,7 @@ class Comment(Base): parent = relation('Comment', backref='children', remote_side=[id]) def __init__(self, text, displayed, username, rating, time, - node=None, parent=None): + node=None, parent=None, proposal=None): self.text = text self.displayed = displayed self.username = username @@ -53,6 +54,7 @@ class Comment(Base): self.time = time self.node = node self.parent = parent + self.proposal = proposal def serializable(self, user_id=None): delta = datetime.now() - self.time @@ -115,41 +117,3 @@ class CommentVote(Base): self.value = value self.user_id = user_id self.comment_id = comment_id - -class Proposal(Base): - __tablename__ = db_prefix + 'proposals' - - id = Column(Integer, primary_key=True) - rating = Column(Integer, nullable=False) - time = Column(DateTime, nullable=False) - text = Column(Text, nullable=False) - displayed = Column(Boolean, index=True, default=False) - username = Column(String(64)) - - node_id = Column(Integer, ForeignKey(db_prefix + 'nodes.id')) - node = relation(Node, backref='proposals') - - def __init__(self, text, displayed, username, rating, time, node): - self.text = text - self.displayed = displayed - self.username = username - self.rating = rating - self.time = time - self.node = node - -class ProposalVote(Base): - __tablename__ = db_prefix + 'proposalvote' - - user_id = Column(Integer, primary_key=True) - # -1 if downvoted, +1 if upvoted, 0 if voted then unvoted. - value = Column(Integer, nullable=False) - - proposal_id = Column(Integer, ForeignKey(db_prefix + 'proposals.id'), - primary_key=True) - proposal = relation(Proposal, backref="votes") - - def __init__(self, proposal_id, user_id, value): - self.value = value - self.user_id = user_id - self.proposal_id = proposal_id - -- cgit v1.2.1 From 81cb2b2a655f4e99311694f68169e12238e5b79b Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 24 Jul 2010 11:35:29 +0100 Subject: Trunk is now 1.1pre. --- CHANGES | 4 ++++ sphinx/__init__.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 8465dae0..1bb0a276 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,7 @@ +Release 1.1 (in development) +============================ + + Release 1.0.1 (in development) ============================== diff --git a/sphinx/__init__.py b/sphinx/__init__.py index 8c1ebed5..31c61a86 100644 --- a/sphinx/__init__.py +++ b/sphinx/__init__.py @@ -12,8 +12,8 @@ import sys from os import path -__version__ = '1.0+' -__released__ = '1.0' # used when Sphinx builds its own docs +__version__ = '1.1pre' +__released__ = '1.1 (hg)' # used when Sphinx builds its own docs package_dir = path.abspath(path.dirname(__file__)) -- cgit v1.2.1 From 96cb318d7cbbbdb33962403050c16cdbae70b2db Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 25 Jul 2010 19:50:20 +0200 Subject: Remove trailing whitespace --- sphinx/websupport/__init__.py | 12 ++++++------ sphinx/websupport/comments/__init__.py | 6 +++--- sphinx/websupport/comments/db.py | 12 ++++++------ sphinx/websupport/comments/sqlalchemystorage.py | 25 ++++++++++++------------- sphinx/websupport/search/whooshsearch.py | 4 ++-- sphinx/websupport/search/xapiansearch.py | 2 +- 6 files changed, 30 insertions(+), 31 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 7c39681a..ddaac1f3 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -44,7 +44,7 @@ class WebSupport(object): self._init_search(search) self._init_storage(storage) - + def _init_storage(self, storage): if isinstance(storage, StorageBackend): self.storage = storage @@ -58,7 +58,7 @@ class WebSupport(object): ensuredir(path.dirname(db_path)) engine = create_engine('sqlite:///%s' % db_path) self.storage = SQLAlchemyStorage(engine) - + def _init_templating(self): import sphinx template_path = path.join(path.dirname(sphinx.__file__), @@ -71,7 +71,7 @@ class WebSupport(object): self.search = search else: mod, cls = search_adapters[search] - search_class = getattr(__import__('sphinx.websupport.search.' + mod, + search_class = getattr(__import__('sphinx.websupport.search.' + mod, None, None, [cls]), cls) search_path = path.join(self.outdir, 'search') self.search = search_class(search_path) @@ -202,15 +202,15 @@ class WebSupport(object): :param rating: the starting rating of the comment, defaults to 0. :param time: the time the comment was created, defaults to now. """ - return self.storage.add_comment(parent_id, text, displayed, + return self.storage.add_comment(parent_id, text, displayed, username, rating, time) - + def get_proposals(self, node_id, user_id=None): return self.storage.get_proposals(node_id, user_id) def add_proposal(self, parent_id, text, displayed=True, username=None, rating=0, time=None): - return self.storage.add_proposal(parent_id, text, displayed, + return self.storage.add_proposal(parent_id, text, displayed, username, rating, time) def process_vote(self, comment_id, user_id, value): diff --git a/sphinx/websupport/comments/__init__.py b/sphinx/websupport/comments/__init__.py index a78a19c1..aca2ac35 100644 --- a/sphinx/websupport/comments/__init__.py +++ b/sphinx/websupport/comments/__init__.py @@ -18,14 +18,14 @@ class StorageBackend(object): `treeloc` is for future use. """ raise NotImplementedError() - + def post_build(self): """Called after a build has completed. Use this to finalize the addition of nodes if needed. """ pass - def add_comment(self, parent_id, text, displayed, username, + def add_comment(self, parent_id, text, displayed, username, rating, time): """Called when a comment is being added.""" raise NotImplementedError() @@ -34,7 +34,7 @@ class StorageBackend(object): """Called to retrieve all comments for a node.""" raise NotImplementedError() - def add_proposal(self, parent_id, text, displayed, username, + def add_proposal(self, parent_id, text, displayed, username, rating, time): raise NotImplementedError() diff --git a/sphinx/websupport/comments/db.py b/sphinx/websupport/comments/db.py index 31f4ff77..35f25a2b 100644 --- a/sphinx/websupport/comments/db.py +++ b/sphinx/websupport/comments/db.py @@ -15,13 +15,13 @@ db_prefix = 'sphinx_' class Node(Base): """Data about a Node in a doctree.""" __tablename__ = db_prefix + 'nodes' - + id = Column(Integer, primary_key=True) document = Column(String(256), nullable=False) line = Column(Integer) source = Column(Text, nullable=False) treeloc = Column(String(32), nullable=False) - + def __init__(self, document, line, source, treeloc): self.document = document self.line = line @@ -44,7 +44,7 @@ class Comment(Base): parent_id = Column(Integer, ForeignKey(db_prefix + 'comments.id')) parent = relation('Comment', backref='children', remote_side=[id]) - def __init__(self, text, displayed, username, rating, time, + def __init__(self, text, displayed, username, rating, time, node=None, parent=None): self.text = text self.displayed = displayed @@ -84,7 +84,7 @@ class Comment(Base): 'vote': vote or 0, 'node': self.node.id if self.node else None, 'parent': self.parent.id if self.parent else None, - 'children': [child.serializable(user_id) + 'children': [child.serializable(user_id) for child in self.children]} def pretty_delta(self, delta): @@ -99,7 +99,7 @@ class Comment(Base): dt = (days, 'day') return '%s %s ago' % dt if dt[0] == 1 else '%s %ss ago' % dt - + class CommentVote(Base): __tablename__ = db_prefix + 'commentvote' @@ -136,7 +136,7 @@ class Proposal(Base): self.rating = rating self.time = time self.node = node - + class ProposalVote(Base): __tablename__ = db_prefix + 'proposalvote' diff --git a/sphinx/websupport/comments/sqlalchemystorage.py b/sphinx/websupport/comments/sqlalchemystorage.py index 10f1fa3c..c57bee63 100644 --- a/sphinx/websupport/comments/sqlalchemystorage.py +++ b/sphinx/websupport/comments/sqlalchemystorage.py @@ -23,28 +23,28 @@ class SQLAlchemyStorage(StorageBackend): self.build_session.commit() self.build_session.close() - def add_comment(self, parent_id, text, displayed, + def add_comment(self, parent_id, text, displayed, username, rating, time): time = time or datetime.now() - + session = Session() - + id = parent_id[1:] if parent_id[0] == 's': node = session.query(Node).filter(Node.id == id).first() - comment = Comment(text, displayed, username, rating, + comment = Comment(text, displayed, username, rating, time, node=node) elif parent_id[0] == 'c': parent = session.query(Comment).filter(Comment.id == id).first() - comment = Comment(text, displayed, username, rating, + comment = Comment(text, displayed, username, rating, time, parent=parent) - + session.add(comment) session.commit() comment = comment.serializable() session.close() return comment - + def get_comments(self, parent_id, user_id): parent_id = parent_id[1:] session = Session() @@ -56,15 +56,15 @@ class SQLAlchemyStorage(StorageBackend): session.close() return comments - def add_proposal(self, parent_id, text, displayed, username, + def add_proposal(self, parent_id, text, displayed, username, rating, time): time = time or datetime.now() - + session = Session() - + node = session.query(Node).filter(Node.id == parent_id).first() proposal= Proposal(text, displayed, username, rating, time, node) - + session.add(proposal) session.commit() session.close() @@ -84,7 +84,7 @@ class SQLAlchemyStorage(StorageBackend): vote = session.query(CommentVote).filter( CommentVote.comment_id == comment_id).filter( CommentVote.user_id == user_id).first() - + comment = session.query(Comment).filter( Comment.id == comment_id).first() @@ -97,4 +97,3 @@ class SQLAlchemyStorage(StorageBackend): session.add(vote) session.commit() session.close() - diff --git a/sphinx/websupport/search/whooshsearch.py b/sphinx/websupport/search/whooshsearch.py index 52f49d8d..658b764d 100644 --- a/sphinx/websupport/search/whooshsearch.py +++ b/sphinx/websupport/search/whooshsearch.py @@ -40,10 +40,10 @@ class WhooshSearch(BaseSearch): def finish_indexing(self): self.index_writer.commit() - + def add_document(self, pagename, title, text): self.index_writer.add_document(path=unicode(pagename), - title=title, + title=title, text=text) def handle_query(self, q): diff --git a/sphinx/websupport/search/xapiansearch.py b/sphinx/websupport/search/xapiansearch.py index f5ad9688..2f2ffbe5 100644 --- a/sphinx/websupport/search/xapiansearch.py +++ b/sphinx/websupport/search/xapiansearch.py @@ -37,7 +37,7 @@ class XapianSearch(BaseSearch): def finish_indexing(self): # Ensure the db lock is removed. del self.database - + def add_document(self, path, title, text): self.database.begin_transaction() # sphinx_page_path is used to easily retrieve documents by path. -- cgit v1.2.1 From b63c357e3467beaf5de70440e391dbc3a858942c Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 25 Jul 2010 19:53:57 +0200 Subject: Fixed line length and indentation of imports --- sphinx/websupport/comments/db.py | 2 +- sphinx/websupport/comments/sqlalchemystorage.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sphinx/websupport/comments/db.py b/sphinx/websupport/comments/db.py index 35f25a2b..b73d6a5a 100644 --- a/sphinx/websupport/comments/db.py +++ b/sphinx/websupport/comments/db.py @@ -1,7 +1,7 @@ from datetime import datetime from sqlalchemy import Column, Integer, Text, String, Boolean, ForeignKey,\ -DateTime + DateTime from sqlalchemy.schema import UniqueConstraint from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relation, sessionmaker diff --git a/sphinx/websupport/comments/sqlalchemystorage.py b/sphinx/websupport/comments/sqlalchemystorage.py index c57bee63..8ee11750 100644 --- a/sphinx/websupport/comments/sqlalchemystorage.py +++ b/sphinx/websupport/comments/sqlalchemystorage.py @@ -1,7 +1,8 @@ from datetime import datetime from sphinx.websupport.comments import StorageBackend -from sphinx.websupport.comments.db import Base, Node, Comment, CommentVote, Session +from sphinx.websupport.comments.db import Base, Node, Comment, CommentVote,\ + Session class SQLAlchemyStorage(StorageBackend): def __init__(self, engine): -- cgit v1.2.1 From 52b4bbad8f4dae7919a7dbba4f96ce7f176ae1c8 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 25 Jul 2010 20:03:28 +0200 Subject: Added missing module docstrings --- sphinx/websupport/comments/__init__.py | 10 ++++++++++ sphinx/websupport/comments/db.py | 13 ++++++++++++- sphinx/websupport/comments/sqlalchemystorage.py | 11 +++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/sphinx/websupport/comments/__init__.py b/sphinx/websupport/comments/__init__.py index aca2ac35..1f160518 100644 --- a/sphinx/websupport/comments/__init__.py +++ b/sphinx/websupport/comments/__init__.py @@ -1,3 +1,13 @@ +# -*- coding: utf-8 -*- +""" + sphinx.websupport.comments + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Comments for the websupport package. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" class StorageBackend(object): def pre_build(self): diff --git a/sphinx/websupport/comments/db.py b/sphinx/websupport/comments/db.py index b73d6a5a..db9ab7a8 100644 --- a/sphinx/websupport/comments/db.py +++ b/sphinx/websupport/comments/db.py @@ -1,3 +1,15 @@ +# -*- coding: utf-8 -*- +""" + sphinx.websupport.comments.db + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + SQLAlchemy table and mapper definitions used by the + :class:`sphinx.websupport.comments.SQLAlchemyStorage`. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + from datetime import datetime from sqlalchemy import Column, Integer, Text, String, Boolean, ForeignKey,\ @@ -152,4 +164,3 @@ class ProposalVote(Base): self.value = value self.user_id = user_id self.proposal_id = proposal_id - diff --git a/sphinx/websupport/comments/sqlalchemystorage.py b/sphinx/websupport/comments/sqlalchemystorage.py index 8ee11750..312663c8 100644 --- a/sphinx/websupport/comments/sqlalchemystorage.py +++ b/sphinx/websupport/comments/sqlalchemystorage.py @@ -1,3 +1,14 @@ +# -*- coding: utf-8 -*- +""" + sphinx.websupport.comments.sqlalchemystorage + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + A SQLAlchemy storage backend. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + from datetime import datetime from sphinx.websupport.comments import StorageBackend -- cgit v1.2.1 From 343ebc46b003bd26d254c31557d7948d4a0773bc Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 25 Jul 2010 20:45:24 +0200 Subject: Removed trailing whitespace --- sphinx/writers/websupport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index 04c989b2..63281f18 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -25,7 +25,7 @@ class WebSupportTranslator(HTMLTranslator): def init_support(self): self.in_commentable = False self.current_id = 0 - + def dispatch_visit(self, node): if node.__class__.__name__ in self.commentable_nodes: self.handle_visit_commentable(node) -- cgit v1.2.1 From 2353aacc71b6cde8f23ef873aaa3dde42e935798 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 25 Jul 2010 23:03:14 +0200 Subject: Remove trailing whitespace --- sphinx/builders/websupport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index e2caeccd..40901eef 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -25,7 +25,7 @@ class WebSupportBuilder(StandaloneHTMLBuilder): def init_translator_class(self): self.translator_class = WebSupportTranslator - + def write_doc(self, docname, doctree): # The translator needs the docname to generate ids. self.cur_docname = docname -- cgit v1.2.1 From 0e6b913a04a0620bb06b86798dd69a018b461d89 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sun, 25 Jul 2010 22:37:53 -0500 Subject: Save proposed changes --- sphinx/websupport/__init__.py | 13 +++++---- sphinx/websupport/comments/__init__.py | 11 ++----- sphinx/websupport/comments/db.py | 2 +- sphinx/websupport/comments/sqlalchemystorage.py | 38 +++++-------------------- 4 files changed, 17 insertions(+), 47 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 7c39681a..452dc295 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -146,10 +146,11 @@ class WebSupport(object): return document def get_comments(self, node_id, user_id=None): - """Get the comments associated with `node_id`. If `user_id` is - given vote information will be included with the returned comments. - The default CommentBackend returns a list of dicts. Each dict - represents a comment, and has the following items: + """Get the comments and source associated with `node_id`. If + `user_id` is given vote information will be included with the + returned comments. The default CommentBackend returns dict with + two keys, *source*, and *comments*. *comments* is a list of + dicts that represent a comment, each having the following items: ============ ====================================================== Key Contents @@ -181,7 +182,7 @@ class WebSupport(object): return self.storage.get_comments(node_id, user_id) def add_comment(self, parent_id, text, displayed=True, username=None, - rating=0, time=None): + rating=0, time=None, proposal=None): """Add a comment to a node or another comment. `parent_id` will have a one letter prefix, distinguishing between node parents and comment parents, 'c' and 's' respectively. This function will @@ -203,7 +204,7 @@ class WebSupport(object): :param time: the time the comment was created, defaults to now. """ return self.storage.add_comment(parent_id, text, displayed, - username, rating, time) + username, rating, time, proposal) def get_proposals(self, node_id, user_id=None): return self.storage.get_proposals(node_id, user_id) diff --git a/sphinx/websupport/comments/__init__.py b/sphinx/websupport/comments/__init__.py index a78a19c1..8cd3c36c 100644 --- a/sphinx/websupport/comments/__init__.py +++ b/sphinx/websupport/comments/__init__.py @@ -25,8 +25,8 @@ class StorageBackend(object): """ pass - def add_comment(self, parent_id, text, displayed, username, - rating, time): + def add_comment(self, parent_id, text, displayed, + username, rating, time, proposal): """Called when a comment is being added.""" raise NotImplementedError() @@ -34,12 +34,5 @@ class StorageBackend(object): """Called to retrieve all comments for a node.""" raise NotImplementedError() - def add_proposal(self, parent_id, text, displayed, username, - rating, time): - raise NotImplementedError() - - def get_proposals(self, parent_id): - raise NotImplementedError() - def process_vote(self, comment_id, user_id, value): raise NotImplementedError() diff --git a/sphinx/websupport/comments/db.py b/sphinx/websupport/comments/db.py index 3a33cb26..3679e3d6 100644 --- a/sphinx/websupport/comments/db.py +++ b/sphinx/websupport/comments/db.py @@ -46,7 +46,7 @@ class Comment(Base): parent = relation('Comment', backref='children', remote_side=[id]) def __init__(self, text, displayed, username, rating, time, - node=None, parent=None, proposal=None): + proposal, node=None, parent=None): self.text = text self.displayed = displayed self.username = username diff --git a/sphinx/websupport/comments/sqlalchemystorage.py b/sphinx/websupport/comments/sqlalchemystorage.py index 10f1fa3c..706ea4c0 100644 --- a/sphinx/websupport/comments/sqlalchemystorage.py +++ b/sphinx/websupport/comments/sqlalchemystorage.py @@ -24,7 +24,7 @@ class SQLAlchemyStorage(StorageBackend): self.build_session.close() def add_comment(self, parent_id, text, displayed, - username, rating, time): + username, rating, time, proposal): time = time or datetime.now() session = Session() @@ -33,11 +33,11 @@ class SQLAlchemyStorage(StorageBackend): if parent_id[0] == 's': node = session.query(Node).filter(Node.id == id).first() comment = Comment(text, displayed, username, rating, - time, node=node) + time, proposal, node=node) elif parent_id[0] == 'c': parent = session.query(Comment).filter(Comment.id == id).first() comment = Comment(text, displayed, username, rating, - time, parent=parent) + time,proposal, parent=parent) session.add(comment) session.commit() @@ -49,35 +49,11 @@ class SQLAlchemyStorage(StorageBackend): parent_id = parent_id[1:] session = Session() node = session.query(Node).filter(Node.id == parent_id).first() - comments = [] - for comment in node.comments: - comments.append(comment.serializable(user_id)) - + data = {'source': node.source, + 'comments': [comment.serializable(user_id) + for comment in node.comments]} session.close() - return comments - - def add_proposal(self, parent_id, text, displayed, username, - rating, time): - time = time or datetime.now() - - session = Session() - - node = session.query(Node).filter(Node.id == parent_id).first() - proposal= Proposal(text, displayed, username, rating, time, node) - - session.add(proposal) - session.commit() - session.close() - return proposal - - def get_proposals(self, parent_id): - session = Session() - node = session.query(Node).filter(Node.id == parent_id).first() - proposals = [] - - # TODO - - return proposals + return data def process_vote(self, comment_id, user_id, value): session = Session() -- cgit v1.2.1 From 4f7ec80f18ca479f87d54b43fdae6f642c8ce98e Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Tue, 27 Jul 2010 16:12:35 -0500 Subject: added ugly proposals --- sphinx/websupport/__init__.py | 72 +++++++++++++++++++++---- sphinx/websupport/comments/__init__.py | 10 +++- sphinx/websupport/comments/db.py | 5 +- sphinx/websupport/comments/sqlalchemystorage.py | 32 ++++++----- 4 files changed, 94 insertions(+), 25 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 452dc295..3fd9d83b 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -10,7 +10,10 @@ """ import cPickle as pickle +import re from os import path +from cgi import escape +from difflib import Differ from datetime import datetime from jinja2 import Environment, FileSystemLoader @@ -203,17 +206,16 @@ class WebSupport(object): :param rating: the starting rating of the comment, defaults to 0. :param time: the time the comment was created, defaults to now. """ - return self.storage.add_comment(parent_id, text, displayed, - username, rating, time, proposal) - - def get_proposals(self, node_id, user_id=None): - return self.storage.get_proposals(node_id, user_id) + id = parent_id[1:] + is_node = parent_id[0] == 's' - def add_proposal(self, parent_id, text, displayed=True, username=None, - rating=0, time=None): - return self.storage.add_proposal(parent_id, text, displayed, - username, rating, time) + node = self.storage.get_node(id) if is_node else None + parent = self.storage.get_comment(id) if not is_node else None + diff = get_diff_html(node.source, proposal) if proposal else None + return self.storage.add_comment(text, displayed, username, rating, + time, proposal, diff, node, parent) + def process_vote(self, comment_id, user_id, value): """Process a user's vote. The web support package relies on the API user to perform authentication. The API user will @@ -240,3 +242,55 @@ class WebSupport(object): """ value = int(value) self.storage.process_vote(comment_id, user_id, value) + +highlight_regex = re.compile(r'([\+\-\^]+)') + +def highlight_text(text, next, tag): + next = next[2:] + new_text = [] + start = 0 + for match in highlight_regex.finditer(next): + new_text.append(text[start:match.start()]) + new_text.append('<%s>' % tag) + new_text.append(text[match.start():match.end()]) + new_text.append('</%s>' % tag) + start = match.end() + new_text.append(text[start:]) + return ''.join(new_text) + +def get_diff_html(source, proposal): + proposal = escape(proposal) + + def handle_line(line, next=None): + prefix = line[0] + text = line[2:] + + if prefix == ' ': + return text + elif prefix == '?': + return '' + + if next[0] == '?': + tag = 'ins' if prefix == '+' else 'del' + text = highlight_text(text, next, tag) + css_class = 'prop_added' if prefix == '+' else 'prop_removed' + + return '<span class="%s">%s</span>\n' % (css_class, text.rstrip()) + + differ = Differ() + diff = list(differ.compare(source.splitlines(1), proposal.splitlines(1))) + + html = [] + line = diff.pop(0) + next = diff.pop(0) + while True: + html.append(handle_line(line, next)) + line = next + try: + next = diff.pop(0) + except IndexError: + handle_line(line) + break + + return ''.join(html) + diff --git a/sphinx/websupport/comments/__init__.py b/sphinx/websupport/comments/__init__.py index 8cd3c36c..0d9fa154 100644 --- a/sphinx/websupport/comments/__init__.py +++ b/sphinx/websupport/comments/__init__.py @@ -19,6 +19,9 @@ class StorageBackend(object): """ raise NotImplementedError() + def get_node(self, node_id): + raise NotImplementedError() + def post_build(self): """Called after a build has completed. Use this to finalize the addition of nodes if needed. @@ -26,11 +29,14 @@ class StorageBackend(object): pass def add_comment(self, parent_id, text, displayed, - username, rating, time, proposal): + username, rating, time, proposal, proposal_diff): """Called when a comment is being added.""" raise NotImplementedError() - def get_comments(self, parent_id): + def get_comment(self, comment_id): + raise NotImplementedError() + + def get_comments(self, parent_id, user_id): """Called to retrieve all comments for a node.""" raise NotImplementedError() diff --git a/sphinx/websupport/comments/db.py b/sphinx/websupport/comments/db.py index 3679e3d6..e55b6af2 100644 --- a/sphinx/websupport/comments/db.py +++ b/sphinx/websupport/comments/db.py @@ -38,6 +38,7 @@ class Comment(Base): displayed = Column(Boolean, index=True, default=False) username = Column(String(64)) proposal = Column(Text) + proposal_diff = Column(Text) node_id = Column(Integer, ForeignKey(db_prefix + 'nodes.id')) node = relation(Node, backref='comments') @@ -46,7 +47,7 @@ class Comment(Base): parent = relation('Comment', backref='children', remote_side=[id]) def __init__(self, text, displayed, username, rating, time, - proposal, node=None, parent=None): + proposal, proposal_diff, node, parent): self.text = text self.displayed = displayed self.username = username @@ -55,6 +56,7 @@ class Comment(Base): self.node = node self.parent = parent self.proposal = proposal + self.proposal_diff = proposal_diff def serializable(self, user_id=None): delta = datetime.now() - self.time @@ -86,6 +88,7 @@ class Comment(Base): 'vote': vote or 0, 'node': self.node.id if self.node else None, 'parent': self.parent.id if self.parent else None, + 'proposal_diff': self.proposal_diff, 'children': [child.serializable(user_id) for child in self.children]} diff --git a/sphinx/websupport/comments/sqlalchemystorage.py b/sphinx/websupport/comments/sqlalchemystorage.py index 706ea4c0..067815a9 100644 --- a/sphinx/websupport/comments/sqlalchemystorage.py +++ b/sphinx/websupport/comments/sqlalchemystorage.py @@ -19,32 +19,39 @@ class SQLAlchemyStorage(StorageBackend): self.build_session.flush() return node.id + def get_node(self, node_id): + session = Session() + node = session.query(Node).filter(Node.id == node_id).first() + session.close() + return node + def post_build(self): self.build_session.commit() self.build_session.close() - def add_comment(self, parent_id, text, displayed, - username, rating, time, proposal): + def add_comment(self, text, displayed, username, rating, time, + proposal, proposal_diff, node=None, parent=None): time = time or datetime.now() session = Session() - id = parent_id[1:] - if parent_id[0] == 's': - node = session.query(Node).filter(Node.id == id).first() - comment = Comment(text, displayed, username, rating, - time, proposal, node=node) - elif parent_id[0] == 'c': - parent = session.query(Comment).filter(Comment.id == id).first() - comment = Comment(text, displayed, username, rating, - time,proposal, parent=parent) - + comment = Comment(text, displayed, username, rating, time, + proposal, proposal_diff, node, parent) + session.add(comment) session.commit() comment = comment.serializable() session.close() return comment + def get_comment(self, comment_id): + session = Session() + comment = session.query(Comment) \ + .filter(Comment.id == comment_id).first() + session.close() + return comment + + def get_comments(self, parent_id, user_id): parent_id = parent_id[1:] session = Session() @@ -73,4 +80,3 @@ class SQLAlchemyStorage(StorageBackend): session.add(vote) session.commit() session.close() - -- cgit v1.2.1 From 099d809fa40a2d4ee1b3177b10f083f4d6936f96 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Tue, 27 Jul 2010 21:16:07 -0500 Subject: fix regression that caused error when replying to a comment --- sphinx/websupport/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index d64c4ff1..9d5f0cba 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -210,7 +210,10 @@ class WebSupport(object): is_node = parent_id[0] == 's' node = self.storage.get_node(id) if is_node else None parent = self.storage.get_comment(id) if not is_node else None - diff = get_diff_html(node.source, proposal) if proposal else None + if node and proposal: + diff = get_diff_html(node.source, proposal) + else: + diff = None return self.storage.add_comment(text, displayed, username, rating, time, proposal, diff, node, parent) -- cgit v1.2.1 From 0124d0c6ece871e705e1b677f9d1aef93132e73a Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Wed, 28 Jul 2010 19:43:30 +0200 Subject: Move item to the correct section. --- CHANGES | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 573b4edd..f1fb982b 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,9 @@ Release 1.1 (in development) ============================ +* Added Python 3.x support. + + Release 1.0.2 (in development) ============================== @@ -23,8 +26,6 @@ Release 1.0.1 (Jul 27, 2010) * Fix hyperrefs in object descriptions for LaTeX. - * Added Python 3.x support. - Release 1.0 (Jul 23, 2010) ========================== -- cgit v1.2.1 From 5cd8ce60a545f81c3899e1c79072d050db55b644 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Wed, 28 Jul 2010 19:49:06 +0200 Subject: Add some changes not picked up in the transplantation process. --- Makefile | 2 +- README | 12 ++++++++++++ doc/intro.rst | 8 ++++---- tests/test_build_html.py | 9 +++++---- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 13228c78..fc1140f3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PYTHON ?= python3 +PYTHON ?= python .PHONY: all check clean clean-pyc clean-patchfiles clean-generated pylint \ reindent test diff --git a/README b/README index bb2dea9d..e31d6b93 100644 --- a/README +++ b/README @@ -26,6 +26,18 @@ Then, direct your browser to ``_build/html/index.html``. Or read them online at <http://sphinx.pocoo.org/>. +Testing +======= + +To run the tests with the interpreter available as ``python``, use:: + + make test + +If you want to use a different interpreter, e.g. ``python3``, use:: + + PYTHON=python3 make test + + Contributing ============ diff --git a/doc/intro.rst b/doc/intro.rst index 1a39e266..c85fbbad 100644 --- a/doc/intro.rst +++ b/doc/intro.rst @@ -45,10 +45,10 @@ See the :ref:`pertinent section in the FAQ list <usingwith>`. Prerequisites ------------- -Sphinx needs at least **Python 2.4** to run, as well as the docutils_ and -Jinja2_ libraries. Sphinx should work with docutils version 0.5 or some -(not broken) SVN trunk snapshot. If you like to have source code highlighting -support, you must also install the Pygments_ library. +Sphinx needs at least **Python 2.4** or **Python 3.1** to run, as well as the +docutils_ and Jinja2_ libraries. Sphinx should work with docutils version 0.5 +or some (not broken) SVN trunk snapshot. If you like to have source code +highlighting support, you must also install the Pygments_ library. .. _reStructuredText: http://docutils.sf.net/rst.html .. _docutils: http://docutils.sf.net/ diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 3ca2c757..0c59d9cc 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -51,6 +51,11 @@ HTML_WARNINGS = ENV_WARNINGS + """\ %(root)s/markup.txt:: WARNING: invalid pair index entry u'keyword; ' """ +if sys.version_info >= (3, 0): + ENV_WARNINGS = remove_unicode_literals(ENV_WARNINGS) + HTML_WARNINGS = remove_unicode_literals(HTML_WARNINGS) + + def tail_check(check): rex = re.compile(check) def checker(nodes): @@ -61,10 +66,6 @@ def tail_check(check): return checker -if sys.version_info >= (3, 0): - ENV_WARNINGS = remove_unicode_literals(ENV_WARNINGS) - HTML_WARNINGS = remove_unicode_literals(HTML_WARNINGS) - HTML_XPATH = { 'images.html': [ (".//img[@src='_images/img.png']", ''), -- cgit v1.2.1 From 584e48c865e00c304e97d677ea2ce34d3d497a85 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Wed, 28 Jul 2010 19:58:17 +0200 Subject: Keep sphinx/__init__.py executable with Python 3. --- sphinx/__init__.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/sphinx/__init__.py b/sphinx/__init__.py index 31c61a86..1ea2e7bf 100644 --- a/sphinx/__init__.py +++ b/sphinx/__init__.py @@ -9,6 +9,9 @@ :license: BSD, see LICENSE for details. """ +# Keep this file executable as-is in Python 3! +# (Otherwise getting the version out of it from setup.py is impossible.) + import sys from os import path @@ -35,13 +38,14 @@ if '+' in __version__ or 'pre' in __version__: def main(argv=sys.argv): if sys.version_info[:3] < (2, 4, 0): - print >>sys.stderr, \ - 'Error: Sphinx requires at least Python 2.4 to run.' + sys.stderr.write('Error: Sphinx requires at least ' + 'Python 2.4 to run.\n') return 1 try: from sphinx import cmdline - except ImportError, err: + except ImportError: + err = sys.exc_info()[1] errstr = str(err) if errstr.lower().startswith('no module named'): whichmod = errstr[16:] @@ -54,14 +58,14 @@ def main(argv=sys.argv): whichmod = 'roman module (which is distributed with Docutils)' hint = ('This can happen if you upgraded docutils using\n' 'easy_install without uninstalling the old version' - 'first.') + 'first.\n') else: whichmod += ' module' - print >>sys.stderr, ('Error: The %s cannot be found. ' - 'Did you install Sphinx and its dependencies ' - 'correctly?' % whichmod) + sys.stderr.write('Error: The %s cannot be found. ' + 'Did you install Sphinx and its dependencies ' + 'correctly?\n' % whichmod) if hint: - print >> sys.stderr, hint + sys.stderr.write(hint) return 1 raise return cmdline.main(argv) -- cgit v1.2.1 From 6a3799564843e0002e6496f799f3efd56fb68e55 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Wed, 28 Jul 2010 19:58:47 +0200 Subject: Ignore distribute files. --- .hgignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgignore b/.hgignore index 40c00aac..b68cccf9 100644 --- a/.hgignore +++ b/.hgignore @@ -16,3 +16,4 @@ \.DS_Store$ ~$ ^utils/.*3\.py$ +^distribute- -- cgit v1.2.1 From c7ece3cb418f04e1a5aae46b33989969e737be2f Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Wed, 28 Jul 2010 20:24:35 +0200 Subject: Fix raw_input which is not converted by 2to3 if not called. --- sphinx/quickstart.py | 8 ++++++-- tests/test_quickstart.py | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/sphinx/quickstart.py b/sphinx/quickstart.py index 557f8c09..92a5bea2 100644 --- a/sphinx/quickstart.py +++ b/sphinx/quickstart.py @@ -22,7 +22,11 @@ from sphinx.util.console import purple, bold, red, turquoise, \ from sphinx.util import texescape # function to get input from terminal -- overridden by the test suite -term_input = raw_input +try: + # this raw_input is not converted by 2to3 + term_input = raw_input +except NameError: + term_input = input PROMPT_PREFIX = '> ' @@ -692,7 +696,7 @@ if sys.version_info >= (3, 0): return _unicode_string_re.sub('\\1', source) for f in ['QUICKSTART_CONF', 'EPUB_CONFIG', 'INTERSPHINX_CONFIG']: - globals()[f] = convert_python_source(globals()[f]) + globals()[f] = _convert_python_source(globals()[f]) del _unicode_string_re, _convert_python_source diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index 72ae764d..541959bd 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -36,8 +36,13 @@ def mock_raw_input(answers, needanswer=False): return '' return raw_input +try: + real_raw_input = raw_input +except NameError: + real_raw_input = input + def teardown_module(): - qs.term_input = raw_input + qs.term_input = real_raw_input qs.TERM_ENCODING = getattr(sys.stdin, 'encoding', None) coloron() -- cgit v1.2.1 From 415e6711dfa2f65bd797891d5e92f332150601d3 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Wed, 28 Jul 2010 20:30:05 +0200 Subject: Update phony targets list. --- Makefile | 4 ++-- sphinx/ext/oldcmarkup.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index fc1140f3..09aa3c96 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ PYTHON ?= python -.PHONY: all check clean clean-pyc clean-patchfiles clean-generated pylint \ - reindent test +.PHONY: all check clean clean-pyc clean-patchfiles clean-backupfiles \ + clean-generated pylint reindent test covertest build convert-utils DONT_CHECK = -i build -i dist -i sphinx/style/jquery.js \ -i sphinx/pycode/pgen2 -i sphinx/util/smartypants.py \ diff --git a/sphinx/ext/oldcmarkup.py b/sphinx/ext/oldcmarkup.py index 84ae61dd..3aa53fd8 100644 --- a/sphinx/ext/oldcmarkup.py +++ b/sphinx/ext/oldcmarkup.py @@ -31,6 +31,7 @@ class OldCDirective(Directive): def run(self): env = self.state.document.settings.env if not env.app._oldcmarkup_warned: + print 'XXXYYY' env.warn(env.docname, WARNING_MSG, self.lineno) env.app._oldcmarkup_warned = True newname = 'c:' + self.name[1:] -- cgit v1.2.1 From 3c786e434a1506b361ec22288e1223050d665c02 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Fri, 30 Jul 2010 16:59:47 +0200 Subject: Run 2to3 on config files which contain Python 2.x unicode literals. --- sphinx/config.py | 27 +++++++++++++------- sphinx/quickstart.py | 8 +++++- sphinx/util/pycompat.py | 67 +++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/sphinx/config.py b/sphinx/config.py index 708da162..efa9f740 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -13,13 +13,20 @@ import os import re import sys from os import path +try: + from distutils.util import run_2to3 +except ImportError: + run_2to3 = None from sphinx.errors import ConfigError from sphinx.util.osutil import make_filename -from sphinx.util.pycompat import bytes, b +from sphinx.util.pycompat import bytes, b, should_run_2to3, run_2to3 nonascii_re = re.compile(b(r'[\x80-\xff]')) +CONFIG_SYNTAX_ERROR = "There is a syntax error in your configuration file: %s" +if sys.version_info >= (3, 0): + CONFIG_SYNTAX_ERROR += "\nDid you change the syntax from 2.x to 3.x?" class Config(object): """Configuration file abstraction.""" @@ -167,15 +174,17 @@ class Config(object): try: try: os.chdir(dirname) - f = open(config_file, 'rb') - try: - code = compile(f.read(), config_file, 'exec') - finally: - f.close() - exec code in config + if should_run_2to3(config_file): + code = run_2to3(config_file) + else: + f = open(config_file, 'rb') + try: + code = f.read() + finally: + f.close() + exec compile(code, config_file, 'exec') in config except SyntaxError, err: - raise ConfigError('There is a syntax error in your ' - 'configuration file: ' + str(err)) + raise ConfigError(CONFIG_SYNTAX_ERROR % err) finally: os.chdir(olddir) diff --git a/sphinx/quickstart.py b/sphinx/quickstart.py index 92a5bea2..7e38a742 100644 --- a/sphinx/quickstart.py +++ b/sphinx/quickstart.py @@ -31,7 +31,13 @@ except NameError: PROMPT_PREFIX = '> ' -QUICKSTART_CONF = '''\ +if sys.version_info >= (3, 0): + # prevents that the file is checked for being written in Python 2.x syntax + QUICKSTART_CONF = '#!/usr/bin/env python3\n' +else: + QUICKSTART_CONF = '' + +QUICKSTART_CONF += '''\ # -*- coding: utf-8 -*- # # %(project)s documentation build configuration file, created by diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index 229b54b4..2ec71e72 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -12,6 +12,7 @@ import sys import codecs import encodings +import re try: from types import ClassType @@ -20,11 +21,6 @@ except ImportError: # Python 3 class_types = (type,) -try: - base_exception = BaseException -except NameError: - base_exception = Exception - # the ubiquitous "bytes" helper function if sys.version_info >= (3, 0): @@ -34,6 +30,66 @@ else: b = str +encoding_re = re.compile(b(r'coding[=:]\s*([-\w.]+)')) +unicode_literal_re = re.compile(ur""" +(?: + "(?:[^"\]]*(?:\\.[^"\\]*)*)"| + '(?:[^'\]]*(?:\\.[^'\\]*)*)' +) +""", re.VERBOSE) + + +try: + from lib2to3.refactor import RefactoringTool, get_fixers_from_package +except ImportError: + _run_2to3 = None + def should_run_2to3(filepath): + return False +else: + def should_run_2to3(filepath): + # th default source code encoding for python 2.x + encoding = 'ascii' + # only the first match of the encoding cookie counts + encoding_set = False + f = open(filepath, 'rb') + try: + for i, line in enumerate(f): + if line.startswith(b('#')): + if i == 0 and b('python3') in line: + return False + if not encoding_set: + encoding_match = encoding_re.match(line) + if encoding_match: + encoding = encoding_match.group(1) + encodin_set = True + elif line.strip(): + try: + line = line.decode(encoding) + except UnicodeDecodeError: + # I'm not sure this will work but let's try it anyway + return True + if unicode_literal_re.search(line) is not None: + return True + finally: + f.close() + return False + + def run_2to3(filepath): + sys.path.append('..') + fixers = get_fixers_from_package('lib2to3.fixes') + fixers.extend(get_fixers_from_package('custom_fixers')) + refactoring_tool = RefactoringTool(fixers) + source = refactoring_tool._read_python_source(filepath)[0] + ast = refactoring_tool.refactor_string(source, 'conf.py') + return unicode(ast) + + +try: + base_exception = BaseException +except NameError: + base_exception = Exception + + try: next = next except NameError: @@ -41,6 +97,7 @@ except NameError: def next(iterator): return iterator.next() + try: bytes = bytes except NameError: -- cgit v1.2.1 From 183316e664af151013dbe07c6bccdeac60d052c1 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Fri, 30 Jul 2010 11:20:43 -0500 Subject: Added test for build and made web support buildable without specifying search adapter --- sphinx/websupport/__init__.py | 21 +++++++-------- sphinx/websupport/search/__init__.py | 5 ++++ sphinx/websupport/search/nullsearch.py | 22 ++++++++++++++++ tests/test_websupport.py | 47 ++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 sphinx/websupport/search/nullsearch.py create mode 100644 tests/test_websupport.py diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 9d5f0cba..4d73c0e1 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -10,7 +10,7 @@ """ import cPickle as pickle -import re +import re, sys from os import path from cgi import escape from difflib import Differ @@ -35,7 +35,7 @@ class WebSupport(object): """ def __init__(self, srcdir='', outdir='', datadir='', search=None, - storage=None): + storage=None, status=sys.stdout, warning=sys.stderr): self.srcdir = srcdir self.outdir = outdir or path.join(self.srcdir, '_build', 'websupport') @@ -43,9 +43,10 @@ class WebSupport(object): self.outdir = outdir or datadir - if search is not None: - self._init_search(search) + self.status = status + self.warning = warning + self._init_search(search) self._init_storage(storage) def _init_storage(self, storage): @@ -73,11 +74,11 @@ class WebSupport(object): if isinstance(search, BaseSearch): self.search = search else: - mod, cls = search_adapters[search] - search_class = getattr(__import__('sphinx.websupport.search.' + mod, - None, None, [cls]), cls) + mod, cls = search_adapters[search or 'null'] + mod = 'sphinx.websupport.search.' + mod + SearchClass = getattr(__import__(mod, None, None, [cls]), cls) search_path = path.join(self.outdir, 'search') - self.search = search_class(search_path) + self.search = SearchClass(search_path) self.results_template = \ self.template_env.get_template('searchresults.html') @@ -95,8 +96,8 @@ class WebSupport(object): doctreedir = path.join(self.outdir, 'doctrees') app = WebSupportApp(self.srcdir, self.srcdir, self.outdir, doctreedir, 'websupport', - search=self.search, - storage=self.storage) + search=self.search, status=self.status, + warning=self.warning, storage=self.storage) self.storage.pre_build() app.build() diff --git a/sphinx/websupport/search/__init__.py b/sphinx/websupport/search/__init__.py index e1d7ea47..d41b560c 100644 --- a/sphinx/websupport/search/__init__.py +++ b/sphinx/websupport/search/__init__.py @@ -12,6 +12,9 @@ import re class BaseSearch(object): + def __init__(self, path): + pass + def init_indexing(self, changed=[]): """Called by the builder to initialize the search indexer. `changed` is a list of pagenames that will be reindexed. You may want to remove @@ -117,4 +120,6 @@ class BaseSearch(object): search_adapters = { 'xapian': ('xapiansearch', 'XapianSearch'), 'whoosh': ('whooshsearch', 'WhooshSearch'), + 'null': ('nullsearch', 'NullSearch') } + diff --git a/sphinx/websupport/search/nullsearch.py b/sphinx/websupport/search/nullsearch.py new file mode 100644 index 00000000..ad3d7dae --- /dev/null +++ b/sphinx/websupport/search/nullsearch.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +""" + sphinx.websupport.search.nullsearch + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + The default search adapter, does nothing. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from sphinx.websupport.search import BaseSearch + +class NullSearchException(Exception): + pass + +class NullSearch(BaseSearch): + def feed(self, pagename, title, doctree): + pass + + def query(self, q): + raise NullSearchException('No search adapter specified.') diff --git a/tests/test_websupport.py b/tests/test_websupport.py new file mode 100644 index 00000000..d9251eb6 --- /dev/null +++ b/tests/test_websupport.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" + test_websupport + ~~~~~~~~~~~~~~~ + + Test the Web Support Package + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import os +from StringIO import StringIO + +from sphinx.websupport import WebSupport + +try: + from functools import wraps +except ImportError: + # functools is new in 2.4 + wraps = lambda f: (lambda w: w) + +from util import * + +def teardown_module(): + (test_root / 'websupport').rmtree(True) + +def with_support(*args, **kwargs): + """Make a WebSupport object and pass it the test.""" + settings = {'srcdir': test_root, + 'outdir': os.path.join(test_root, 'websupport'), + 'status': StringIO(), + 'warning': StringIO()} + settings.update(kwargs) + + def generator(func): + @wraps(func) + def new_func(*args2, **kwargs2): + support = WebSupport(**settings) + func(support, *args2, **kwargs2) + return new_func + return generator + +@with_support() +def test_build(support): + support.build() + -- cgit v1.2.1 From 85a526cad0212a10614c74597df1e6e36af91a56 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Fri, 30 Jul 2010 15:53:02 -0500 Subject: Added DocumentNotFoundError --- sphinx/websupport/__init__.py | 9 ++++++++- tests/test_websupport.py | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 4d73c0e1..6c292070 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -22,6 +22,7 @@ from sphinx.application import Sphinx from sphinx.util.osutil import ensuredir from sphinx.websupport.search import BaseSearch, search_adapters from sphinx.websupport.comments import StorageBackend +from sphinx.websupport.errors import DocumentNotFoundError class WebSupportApp(Sphinx): def __init__(self, *args, **kwargs): @@ -126,7 +127,13 @@ class WebSupport(object): :param docname: the name of the document to load. """ infilename = path.join(self.outdir, docname + '.fpickle') - f = open(infilename, 'rb') + + try: + f = open(infilename, 'rb') + except IOError: + raise DocumentNotFoundError( + 'The document "%s" could not be found' % docname) + document = pickle.load(f) return document diff --git a/tests/test_websupport.py b/tests/test_websupport.py index d9251eb6..80a4affe 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -13,6 +13,7 @@ import os from StringIO import StringIO from sphinx.websupport import WebSupport +from sphinx.websupport.errors import DocumentNotFoundError try: from functools import wraps @@ -44,4 +45,5 @@ def with_support(*args, **kwargs): @with_support() def test_build(support): support.build() + raises(DocumentNotFoundError, support.get_document, 'nonexisting') -- cgit v1.2.1 From 25010fc7cbaed5565137fcbdbfbbf88f7242badf Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Fri, 30 Jul 2010 16:13:51 -0500 Subject: Added SrcdirNotSpecifiedError --- sphinx/websupport/__init__.py | 5 ++++- tests/test_websupport.py | 19 ++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 6c292070..ae55ee2e 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -22,7 +22,7 @@ from sphinx.application import Sphinx from sphinx.util.osutil import ensuredir from sphinx.websupport.search import BaseSearch, search_adapters from sphinx.websupport.comments import StorageBackend -from sphinx.websupport.errors import DocumentNotFoundError +from sphinx.websupport.errors import * class WebSupportApp(Sphinx): def __init__(self, *args, **kwargs): @@ -94,6 +94,9 @@ class WebSupport(object): build the pickles and search index, placing them into `outdir`. It will also save node data to the database. """ + if not self.srcdir: + raise SrcdirNotSpecifiedError( \ + 'No srcdir associated with WebSupport object') doctreedir = path.join(self.outdir, 'doctrees') app = WebSupportApp(self.srcdir, self.srcdir, self.outdir, doctreedir, 'websupport', diff --git a/tests/test_websupport.py b/tests/test_websupport.py index 80a4affe..b8884bf9 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -13,7 +13,7 @@ import os from StringIO import StringIO from sphinx.websupport import WebSupport -from sphinx.websupport.errors import DocumentNotFoundError +from sphinx.websupport.errors import * try: from functools import wraps @@ -23,13 +23,14 @@ except ImportError: from util import * + def teardown_module(): (test_root / 'websupport').rmtree(True) + def with_support(*args, **kwargs): """Make a WebSupport object and pass it the test.""" - settings = {'srcdir': test_root, - 'outdir': os.path.join(test_root, 'websupport'), + settings = {'outdir': os.path.join(test_root, 'websupport'), 'status': StringIO(), 'warning': StringIO()} settings.update(kwargs) @@ -42,8 +43,20 @@ def with_support(*args, **kwargs): return new_func return generator + @with_support() +def test_no_srcdir(support): + """Make sure the correct exception is raised if srcdir is not given.""" + raises(SrcdirNotSpecifiedError, support.build) + +@with_support(srcdir=test_root) def test_build(support): support.build() + +@with_support() +def test_get_document(support): raises(DocumentNotFoundError, support.get_document, 'nonexisting') + contents = support.get_document('contents') + assert contents['title'] and contents['body'] \ + and contents['sidebar'] and contents['relbar'] -- cgit v1.2.1 From 26cf7c405b684d0e1cb98f183f5d73153164d408 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Fri, 30 Jul 2010 16:15:41 -0500 Subject: Somehow I always forget to add files... (sphinx.websupport.errors) --- sphinx/websupport/errors.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 sphinx/websupport/errors.py diff --git a/sphinx/websupport/errors.py b/sphinx/websupport/errors.py new file mode 100644 index 00000000..b1c47915 --- /dev/null +++ b/sphinx/websupport/errors.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +""" + sphinx.websupport.errors + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Contains Error classes for the web support package. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +__all__ = ['DocumentNotFoundError', 'SrcdirNotSpecifiedError'] + +class DocumentNotFoundError(Exception): + pass + + +class SrcdirNotSpecifiedError(Exception): + pass -- cgit v1.2.1 From 6f9ebcceb52eb563082c4c4ac9d2628182322314 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Fri, 30 Jul 2010 17:07:48 -0500 Subject: Added basic search tests. --- tests/test_websupport.py | 50 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/tests/test_websupport.py b/tests/test_websupport.py index b8884bf9..3d61e55c 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -9,7 +9,7 @@ :license: BSD, see LICENSE for details. """ -import os +import os, sys from StringIO import StringIO from sphinx.websupport import WebSupport @@ -24,15 +24,22 @@ except ImportError: from util import * -def teardown_module(): +default_settings = {'outdir': os.path.join(test_root, 'websupport'), + 'status': StringIO(), + 'warning': StringIO()} + + +def clear_builddir(): (test_root / 'websupport').rmtree(True) +def teardown_module(): + clear_builddir() + + def with_support(*args, **kwargs): """Make a WebSupport object and pass it the test.""" - settings = {'outdir': os.path.join(test_root, 'websupport'), - 'status': StringIO(), - 'warning': StringIO()} + settings = default_settings.copy() settings.update(kwargs) def generator(func): @@ -49,10 +56,12 @@ def test_no_srcdir(support): """Make sure the correct exception is raised if srcdir is not given.""" raises(SrcdirNotSpecifiedError, support.build) + @with_support(srcdir=test_root) def test_build(support): support.build() + @with_support() def test_get_document(support): raises(DocumentNotFoundError, support.get_document, 'nonexisting') @@ -60,3 +69,34 @@ def test_get_document(support): contents = support.get_document('contents') assert contents['title'] and contents['body'] \ and contents['sidebar'] and contents['relbar'] + + +def search_adapter_helper(adapter): + clear_builddir() + + settings = default_settings.copy() + settings.update({'srcdir': test_root, + 'search': adapter}) + support = WebSupport(**settings) + + support.build() + + +def test_xapian(): + # Don't run tests if xapian is not installed. + try: + import xapian + search_adapter_helper('xapian') + except ImportError: + sys.stderr.write('info: not running xapian tests, ' \ + 'xapian doesn\'t seem to be installed') + + +def test_whoosh(): + # Don't run tests if xapian is not installed. + try: + import whoosh + search_adapter_helper('whoosh') + except ImportError: + sys.stderr.write('info: not running xapian tests, ' \ + 'whoosh doesn\'t seem to be installed') -- cgit v1.2.1 From d22b8f72cc5ed30234fd4148f7d540ab3730b2a4 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 31 Jul 2010 12:23:26 -0500 Subject: More complete tests for search adapters. --- sphinx/websupport/search/__init__.py | 2 +- sphinx/websupport/search/whooshsearch.py | 2 ++ tests/test_websupport.py | 25 ++++++++++++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/sphinx/websupport/search/__init__.py b/sphinx/websupport/search/__init__.py index d41b560c..0e613222 100644 --- a/sphinx/websupport/search/__init__.py +++ b/sphinx/websupport/search/__init__.py @@ -83,7 +83,7 @@ class BaseSearch(object): query `q`. This should return an iterable containing tuples of the following format:: - (<path>, <title> <context>) + (<path>, <title>, <context>) `path` and `title` are the same values that were passed to :meth:`add_document`, and `context` should be a short text snippet diff --git a/sphinx/websupport/search/whooshsearch.py b/sphinx/websupport/search/whooshsearch.py index 658b764d..257393a6 100644 --- a/sphinx/websupport/search/whooshsearch.py +++ b/sphinx/websupport/search/whooshsearch.py @@ -40,6 +40,8 @@ class WhooshSearch(BaseSearch): def finish_indexing(self): self.index_writer.commit() + # Create a new searcher so changes can be seen immediately + self.searcher = self.index.searcher() def add_document(self, pagename, title, text): self.index_writer.add_document(path=unicode(pagename), diff --git a/tests/test_websupport.py b/tests/test_websupport.py index 3d61e55c..3f352cd6 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -78,9 +78,32 @@ def search_adapter_helper(adapter): settings.update({'srcdir': test_root, 'search': adapter}) support = WebSupport(**settings) - support.build() + s = support.search + + # Test the adapters query method. A search for "Epigraph" should return + # one result. + results = s.query(u'Epigraph') + assert len(results) == 1, \ + '%s search adapter returned %s search result(s), should have been 1'\ + % (adapter, len(results)) + + # Make sure documents are properly updated by the search adapter. + s.init_indexing(changed=['markup']) + s.add_document(u'markup', u'title', u'SomeLongRandomWord') + s.finish_indexing() + # Now a search for "Epigraph" should return zero results. + results = s.query(u'Epigraph') + assert len(results) == 0, \ + '%s search adapter returned %s search result(s), should have been 0'\ + % (adapter, len(results)) + # A search for "SomeLongRandomWord" should return one result. + results = s.query(u'SomeLongRandomWord') + assert len(results) == 1, \ + '%s search adapter returned %s search result(s), should have been 1'\ + % (adapter, len(results)) + def test_xapian(): # Don't run tests if xapian is not installed. -- cgit v1.2.1 From a1706a0d3d654e605dda77b2649327bf7066dc02 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 31 Jul 2010 19:47:15 +0200 Subject: Improve support for automatic 2to3 conversion of config files. It now kicks in whenever the original file raises SyntaxErrors on compiling. --- sphinx/config.py | 37 ++++++++++++++++------------ sphinx/util/pycompat.py | 65 +++++++++++++------------------------------------ tests/test_config.py | 6 +++++ 3 files changed, 45 insertions(+), 63 deletions(-) diff --git a/sphinx/config.py b/sphinx/config.py index efa9f740..6c27f85f 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -13,14 +13,10 @@ import os import re import sys from os import path -try: - from distutils.util import run_2to3 -except ImportError: - run_2to3 = None from sphinx.errors import ConfigError from sphinx.util.osutil import make_filename -from sphinx.util.pycompat import bytes, b, should_run_2to3, run_2to3 +from sphinx.util.pycompat import bytes, b, convert_with_2to3 nonascii_re = re.compile(b(r'[\x80-\xff]')) @@ -172,17 +168,28 @@ class Config(object): config['tags'] = tags olddir = os.getcwd() try: + # we promise to have the config dir as current dir while the + # config file is executed + os.chdir(dirname) + # get config source + f = open(config_file, 'rb') try: - os.chdir(dirname) - if should_run_2to3(config_file): - code = run_2to3(config_file) - else: - f = open(config_file, 'rb') - try: - code = f.read() - finally: - f.close() - exec compile(code, config_file, 'exec') in config + source = f.read() + finally: + f.close() + try: + # compile to a code object, handle syntax errors + try: + code = compile(source, config_file, 'exec') + except SyntaxError: + if convert_with_2to3: + # maybe the file uses 2.x syntax; try to refactor to + # 3.x syntax using 2to3 + source = convert_with_2to3(config_file) + code = compile(source, config_file, 'exec') + else: + raise + exec code in config except SyntaxError, err: raise ConfigError(CONFIG_SYNTAX_ERROR % err) finally: diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index 2ec71e72..5f23bbe1 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -30,58 +30,26 @@ else: b = str -encoding_re = re.compile(b(r'coding[=:]\s*([-\w.]+)')) -unicode_literal_re = re.compile(ur""" -(?: - "(?:[^"\]]*(?:\\.[^"\\]*)*)"| - '(?:[^'\]]*(?:\\.[^'\\]*)*)' -) -""", re.VERBOSE) +# Support for running 2to3 over config files - -try: - from lib2to3.refactor import RefactoringTool, get_fixers_from_package -except ImportError: - _run_2to3 = None - def should_run_2to3(filepath): - return False +if sys.version_info < (3, 0): + # no need to refactor on 2.x versions + convert_with_2to3 = None else: - def should_run_2to3(filepath): - # th default source code encoding for python 2.x - encoding = 'ascii' - # only the first match of the encoding cookie counts - encoding_set = False - f = open(filepath, 'rb') - try: - for i, line in enumerate(f): - if line.startswith(b('#')): - if i == 0 and b('python3') in line: - return False - if not encoding_set: - encoding_match = encoding_re.match(line) - if encoding_match: - encoding = encoding_match.group(1) - encodin_set = True - elif line.strip(): - try: - line = line.decode(encoding) - except UnicodeDecodeError: - # I'm not sure this will work but let's try it anyway - return True - if unicode_literal_re.search(line) is not None: - return True - finally: - f.close() - return False - - def run_2to3(filepath): - sys.path.append('..') + def convert_with_2to3(filepath): + from lib2to3.refactor import RefactoringTool, get_fixers_from_package + from lib2to3.pgen2.parse import ParseError fixers = get_fixers_from_package('lib2to3.fixes') - fixers.extend(get_fixers_from_package('custom_fixers')) refactoring_tool = RefactoringTool(fixers) source = refactoring_tool._read_python_source(filepath)[0] - ast = refactoring_tool.refactor_string(source, 'conf.py') - return unicode(ast) + try: + tree = refactoring_tool.refactor_string(source, 'conf.py') + except ParseError, err: + # do not propagate lib2to3 exceptions + lineno, offset = err.context[1] + # try to match ParseError details with SyntaxError details + raise SyntaxError(err.msg, (filepath, lineno, offset, err.value)) + return unicode(tree) try: @@ -93,7 +61,8 @@ except NameError: try: next = next except NameError: - # this is on Python 2, where the method is called "next" + # this is on Python 2, where the method is called "next" (it is refactored + # to __next__ by 2to3, but in that case never executed) def next(iterator): return iterator.next() diff --git a/tests/test_config.py b/tests/test_config.py index ecf90f60..7fce4495 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -88,6 +88,12 @@ def test_errors_warnings(dir): write_file(dir / 'conf.py', u'project = \n', 'ascii') 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') + cfg = Config(dir, 'conf.py', {}, None) + cfg.init_values() + assert cfg.project == u'Jägermeister' + # test the warning for bytestrings with non-ascii content # bytestrings with non-ascii content are a syntax error in python3 so we # skip the test there -- cgit v1.2.1 From 8981baef10bc49cc4feeef747bb867b7b98077df Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 31 Jul 2010 15:51:13 -0500 Subject: Added test for comment system. --- tests/test_websupport.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/test_websupport.py b/tests/test_websupport.py index 3f352cd6..53307fb4 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -14,6 +14,8 @@ from StringIO import StringIO from sphinx.websupport import WebSupport from sphinx.websupport.errors import * +from sphinx.websupport.comments.sqlalchemystorage import Session +from sphinx.websupport.comments.db import Node try: from functools import wraps @@ -116,10 +118,25 @@ def test_xapian(): def test_whoosh(): - # Don't run tests if xapian is not installed. + # Don't run tests if whoosh is not installed. try: import whoosh search_adapter_helper('whoosh') except ImportError: - sys.stderr.write('info: not running xapian tests, ' \ + sys.stderr.write('info: not running whoosh tests, ' \ 'whoosh doesn\'t seem to be installed') + + +@with_support() +def test_comments(support): + session = Session() + node = session.query(Node).first() + comment = support.add_comment('First test comment', node=str(node.id)) + support.add_comment('Child test comment', parent=str(comment['id'])) + data = support.get_comments(str(node.id)) + comments = data['comments'] + children = comments[0]['children'] + assert len(comments) == 1 + assert comments[0]['text'] == 'First test comment' + assert len(children) == 1 + assert children[0]['text'] == 'Child test comment' -- cgit v1.2.1 From c3dee3315bd921db57bd808f2176e395bba7f8fe Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 31 Jul 2010 15:53:13 -0500 Subject: refactored add_comment and get_comments; added CombinedHtmlDiff class --- sphinx/websupport/__init__.py | 91 +++++-------------------- sphinx/websupport/comments/__init__.py | 10 +-- sphinx/websupport/comments/differ.py | 66 ++++++++++++++++++ sphinx/websupport/comments/sqlalchemystorage.py | 41 ++++++----- 4 files changed, 105 insertions(+), 103 deletions(-) create mode 100644 sphinx/websupport/comments/differ.py diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index ae55ee2e..7af8aa1c 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -9,11 +9,9 @@ :license: BSD, see LICENSE for details. """ +import sys import cPickle as pickle -import re, sys from os import path -from cgi import escape -from difflib import Differ from datetime import datetime from jinja2 import Environment, FileSystemLoader @@ -195,20 +193,25 @@ class WebSupport(object): """ return self.storage.get_comments(node_id, user_id) - def add_comment(self, parent_id, text, displayed=True, username=None, - rating=0, time=None, proposal=None): - """Add a comment to a node or another comment. `parent_id` will have - a one letter prefix, distinguishing between node parents and - comment parents, 'c' and 's' respectively. This function will - return the comment in the same format as :meth:`get_comments`. - Usage is simple:: + def add_comment(self, text, node='', parent='', displayed=True, + username=None, rating=0, time=None, proposal=None): + """Add a comment to a node or another comment. Returns the comment + in the same format as :meth:`get_comments`. If the comment is being + attached to a node, pass in the node's id (as a string) with the + node keyword argument:: + + comment = support.add_comment(text, node=node_id) + + If the comment is the child of another comment, provide the parent's + id (as a string) with the parent keyword argument:: + + comment = support.add_comment(text, parent=parent_id) - comment = support.add_comment(parent_id, text) - If you would like to store a username with the comment, pass in the optional `username` keyword argument:: - comment = support.add_comment(parent_id, text, username=username) + comment = support.add_comment(text, node=node_id, + username=username) :param parent_id: the prefixed id of the comment's parent. :param text: the text of the comment. @@ -217,16 +220,8 @@ class WebSupport(object): :param rating: the starting rating of the comment, defaults to 0. :param time: the time the comment was created, defaults to now. """ - id = parent_id[1:] - is_node = parent_id[0] == 's' - node = self.storage.get_node(id) if is_node else None - parent = self.storage.get_comment(id) if not is_node else None - if node and proposal: - diff = get_diff_html(node.source, proposal) - else: - diff = None return self.storage.add_comment(text, displayed, username, rating, - time, proposal, diff, node, parent) + time, proposal, node, parent) def process_vote(self, comment_id, user_id, value): """Process a user's vote. The web support package relies @@ -254,55 +249,3 @@ class WebSupport(object): """ value = int(value) self.storage.process_vote(comment_id, user_id, value) - -highlight_regex = re.compile(r'([\+\-\^]+)') - -def highlight_text(text, next, tag): - next = next[2:] - new_text = [] - start = 0 - for match in highlight_regex.finditer(next): - new_text.append(text[start:match.start()]) - new_text.append('<%s>' % tag) - new_text.append(text[match.start():match.end()]) - new_text.append('</%s>' % tag) - start = match.end() - new_text.append(text[start:]) - return ''.join(new_text) - -def get_diff_html(source, proposal): - proposal = escape(proposal) - - def handle_line(line, next=None): - prefix = line[0] - text = line[2:] - - if prefix == ' ': - return text - elif prefix == '?': - return '' - - if next[0] == '?': - tag = 'ins' if prefix == '+' else 'del' - text = highlight_text(text, next, tag) - css_class = 'prop_added' if prefix == '+' else 'prop_removed' - - return '<span class="%s">%s</span>\n' % (css_class, text.rstrip()) - - differ = Differ() - diff = list(differ.compare(source.splitlines(1), proposal.splitlines(1))) - - html = [] - line = diff.pop(0) - next = diff.pop(0) - while True: - html.append(handle_line(line, next)) - line = next - try: - next = diff.pop(0) - except IndexError: - handle_line(line) - break - - return ''.join(html) - diff --git a/sphinx/websupport/comments/__init__.py b/sphinx/websupport/comments/__init__.py index 2daf9b64..3d7f5154 100644 --- a/sphinx/websupport/comments/__init__.py +++ b/sphinx/websupport/comments/__init__.py @@ -29,23 +29,17 @@ class StorageBackend(object): """ raise NotImplementedError() - def get_node(self, node_id): - raise NotImplementedError() - def post_build(self): """Called after a build has completed. Use this to finalize the addition of nodes if needed. """ pass - def add_comment(self, parent_id, text, displayed, - username, rating, time, proposal, proposal_diff): + def add_comment(self, text, displayed, username, rating, time, + proposal, node, parent): """Called when a comment is being added.""" raise NotImplementedError() - def get_comment(self, comment_id): - raise NotImplementedError() - def get_comments(self, parent_id, user_id): """Called to retrieve all comments for a node.""" raise NotImplementedError() diff --git a/sphinx/websupport/comments/differ.py b/sphinx/websupport/comments/differ.py new file mode 100644 index 00000000..78692234 --- /dev/null +++ b/sphinx/websupport/comments/differ.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +""" + sphinx.websupport.comments.differ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + A differ for creating an HTML representations of proposal diffs + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import re +from cgi import escape +from difflib import Differ + +class CombinedHtmlDiff(object): + + highlight_regex = re.compile(r'([\+\-\^]+)') + + def _highlight_text(self, text, next, tag): + next = next[2:] + new_text = [] + start = 0 + for match in highlight_regex.finditer(next): + new_text.append(text[start:match.start()]) + new_text.append('<%s>' % tag) + new_text.append(text[match.start():match.end()]) + new_text.append('</%s>' % tag) + start = match.end() + new_text.append(text[start:]) + return ''.join(new_text) + + def _handle_line(line, next=None): + prefix = line[0] + text = line[2:] + + if prefix == ' ': + return text + elif prefix == '?': + return '' + + if next[0] == '?': + tag = 'ins' if prefix == '+' else 'del' + text = self._highlight_text(text, next, tag) + css_class = 'prop_added' if prefix == '+' else 'prop_removed' + + return '<span class="%s">%s</span>\n' % (css_class, text.rstrip()) + + def make_html(self, source, proposal): + proposal = escape(proposal) + + differ = Differ() + diff = list(differ.compare(source.splitlines(1), + proposal.splitlines(1))) + html = [] + line = diff.pop(0) + next = diff.pop(0) + while True: + html.append(self._handle_line(line, next)) + line = next + try: + next = diff.pop(0) + except IndexError: + self._handle_line(line) + break + return ''.join(html) diff --git a/sphinx/websupport/comments/sqlalchemystorage.py b/sphinx/websupport/comments/sqlalchemystorage.py index 6c9b06b3..294c48f4 100644 --- a/sphinx/websupport/comments/sqlalchemystorage.py +++ b/sphinx/websupport/comments/sqlalchemystorage.py @@ -14,6 +14,7 @@ from datetime import datetime from sphinx.websupport.comments import StorageBackend from sphinx.websupport.comments.db import Base, Node, Comment, CommentVote,\ Session +from sphinx.websupport.comments.differ import CombinedHtmlDiff class SQLAlchemyStorage(StorageBackend): def __init__(self, engine): @@ -31,41 +32,39 @@ class SQLAlchemyStorage(StorageBackend): self.build_session.flush() return node.id - def get_node(self, node_id): - session = Session() - node = session.query(Node).filter(Node.id == node_id).first() - session.close() - return node - def post_build(self): self.build_session.commit() self.build_session.close() def add_comment(self, text, displayed, username, rating, time, - proposal, proposal_diff, node=None, parent=None): - time = time or datetime.now() - + proposal, node, parent): session = Session() - comment = Comment(text, displayed, username, rating, time, - proposal, proposal_diff, node, parent) + if node: + node = session.query(Node).filter(Node.id == node).first() + parent = None + else: + node = None + parent = session.query(Comment).filter( + Comment.id == parent).first() + if node and proposal: + differ = CombinedHtmlDiff() + proposal_diff = differ.make_html(node.source, proposal) + else: + proposal_diff = None + + comment = Comment(text, displayed, username, rating, + time or datetime.now(), proposal, proposal_diff, + node, parent) session.add(comment) session.commit() comment = comment.serializable() session.close() return comment - def get_comment(self, comment_id): - session = Session() - comment = session.query(Comment) \ - .filter(Comment.id == comment_id).first() - session.close() - return comment - - def get_comments(self, parent_id, user_id): - parent_id = parent_id[1:] + def get_comments(self, node_id, user_id): session = Session() - node = session.query(Node).filter(Node.id == parent_id).first() + node = session.query(Node).filter(Node.id == node_id).first() data = {'source': node.source, 'comments': [comment.serializable(user_id) for comment in node.comments]} -- cgit v1.2.1 From 7a6299bc550a204ca654dd0eaa00f2c1a13b6906 Mon Sep 17 00:00:00 2001 From: jacob <jacob@panther> Date: Tue, 3 Aug 2010 12:21:43 -0500 Subject: Converted comment schema from adjacency list to materialized path. Added tests for commments. Layed groundwork for comment moderation. --- sphinx/websupport/__init__.py | 20 ++++--- sphinx/websupport/comments/__init__.py | 2 +- sphinx/websupport/comments/db.py | 52 +++++++--------- sphinx/websupport/comments/sqlalchemystorage.py | 79 ++++++++++++++++++------- tests/test_websupport.py | 57 ++++++++++++++++-- 5 files changed, 147 insertions(+), 63 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 7af8aa1c..373622eb 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -157,7 +157,7 @@ class WebSupport(object): document['title'] = 'Search Results' return document - def get_comments(self, node_id, user_id=None): + def get_comments(self, node_id, username=None, moderator=False): """Get the comments and source associated with `node_id`. If `user_id` is given vote information will be included with the returned comments. The default CommentBackend returns dict with @@ -191,10 +191,11 @@ class WebSupport(object): :param node_id: the id of the node to get comments for. :param user_id: the id of the user viewing the comments. """ - return self.storage.get_comments(node_id, user_id) + return self.storage.get_comments(node_id, username, moderator) - def add_comment(self, text, node='', parent='', displayed=True, - username=None, rating=0, time=None, proposal=None): + def add_comment(self, text, node_id='', parent_id='', displayed=True, + username=None, rating=0, time=None, proposal=None, + moderator=False): """Add a comment to a node or another comment. Returns the comment in the same format as :meth:`get_comments`. If the comment is being attached to a node, pass in the node's id (as a string) with the @@ -215,15 +216,16 @@ class WebSupport(object): :param parent_id: the prefixed id of the comment's parent. :param text: the text of the comment. - :param displayed: for future use... + :param displayed: for moderation purposes :param username: the username of the user making the comment. :param rating: the starting rating of the comment, defaults to 0. :param time: the time the comment was created, defaults to now. """ return self.storage.add_comment(text, displayed, username, rating, - time, proposal, node, parent) + time, proposal, node_id, parent_id, + moderator) - def process_vote(self, comment_id, user_id, value): + def process_vote(self, comment_id, username, value): """Process a user's vote. The web support package relies on the API user to perform authentication. The API user will typically receive a comment_id and value from a form, and then @@ -248,4 +250,6 @@ class WebSupport(object): :param value: 1 for an upvote, -1 for a downvote, 0 for an unvote. """ value = int(value) - self.storage.process_vote(comment_id, user_id, value) + if not -1 <= value <= 1: + raise ValueError('vote value %s out of range (-1, 1)' % value) + self.storage.process_vote(comment_id, username, value) diff --git a/sphinx/websupport/comments/__init__.py b/sphinx/websupport/comments/__init__.py index 3d7f5154..10856dff 100644 --- a/sphinx/websupport/comments/__init__.py +++ b/sphinx/websupport/comments/__init__.py @@ -40,7 +40,7 @@ class StorageBackend(object): """Called when a comment is being added.""" raise NotImplementedError() - def get_comments(self, parent_id, user_id): + def get_comments(self, parent_id, user_id, moderator): """Called to retrieve all comments for a node.""" raise NotImplementedError() diff --git a/sphinx/websupport/comments/db.py b/sphinx/websupport/comments/db.py index ecb62afa..91175ed3 100644 --- a/sphinx/websupport/comments/db.py +++ b/sphinx/websupport/comments/db.py @@ -32,7 +32,6 @@ class Node(Base): document = Column(String(256), nullable=False) line = Column(Integer) source = Column(Text, nullable=False) - treeloc = Column(String(32), nullable=False) def __init__(self, document, line, source, treeloc): self.document = document @@ -51,26 +50,32 @@ class Comment(Base): username = Column(String(64)) proposal = Column(Text) proposal_diff = Column(Text) + path = Column(String(256), index=True) - node_id = Column(Integer, ForeignKey(db_prefix + 'nodes.id')) - node = relation(Node, backref='comments') - - parent_id = Column(Integer, ForeignKey(db_prefix + 'comments.id')) - parent = relation('Comment', backref='children', remote_side=[id]) + #node_id = Column(Integer, ForeignKey(db_prefix + 'nodes.id')) + #node = relation(Node, backref='comments') def __init__(self, text, displayed, username, rating, time, - proposal, proposal_diff, node, parent): + proposal, proposal_diff): self.text = text self.displayed = displayed self.username = username self.rating = rating self.time = time - self.node = node - self.parent = parent self.proposal = proposal self.proposal_diff = proposal_diff - def serializable(self, user_id=None): + def set_path(self, node_id, parent_id): + if node_id: + self.path = '%s.%s' % (node_id, self.id) + else: + session = Session() + parent_path = session.query(Comment.path).\ + filter(Comment.id == parent_id).one().path + session.close() + self.path = '%s.%s' % (parent_path, self.id) + + def serializable(self, vote=0): delta = datetime.now() - self.time time = {'year': self.time.year, @@ -82,15 +87,6 @@ class Comment(Base): 'iso': self.time.isoformat(), 'delta': self.pretty_delta(delta)} - vote = '' - if user_id is not None: - session = Session() - vote = session.query(CommentVote).filter( - CommentVote.comment_id == self.id).filter( - CommentVote.user_id == user_id).first() - vote = vote.value if vote is not None else 0 - session.close() - return {'text': self.text, 'username': self.username or 'Anonymous', 'id': self.id, @@ -98,11 +94,8 @@ class Comment(Base): 'age': delta.seconds, 'time': time, 'vote': vote or 0, - 'node': self.node.id if self.node else None, - 'parent': self.parent.id if self.parent else None, 'proposal_diff': self.proposal_diff, - 'children': [child.serializable(user_id) - for child in self.children]} + 'children': []} def pretty_delta(self, delta): days = delta.days @@ -120,15 +113,14 @@ class Comment(Base): class CommentVote(Base): __tablename__ = db_prefix + 'commentvote' - user_id = Column(Integer, primary_key=True) - # -1 if downvoted, +1 if upvoted, 0 if voted then unvoted. - value = Column(Integer, nullable=False) - + username = Column(String(64), primary_key=True) comment_id = Column(Integer, ForeignKey(db_prefix + 'comments.id'), primary_key=True) comment = relation(Comment, backref="votes") + # -1 if downvoted, +1 if upvoted, 0 if voted then unvoted. + value = Column(Integer, nullable=False) - def __init__(self, comment_id, user_id, value): - self.value = value - self.user_id = user_id + def __init__(self, comment_id, username, value): self.comment_id = comment_id + self.username = username + self.value = value diff --git a/sphinx/websupport/comments/sqlalchemystorage.py b/sphinx/websupport/comments/sqlalchemystorage.py index 294c48f4..0e11c4a0 100644 --- a/sphinx/websupport/comments/sqlalchemystorage.py +++ b/sphinx/websupport/comments/sqlalchemystorage.py @@ -11,6 +11,7 @@ from datetime import datetime +from sqlalchemy.orm import aliased from sphinx.websupport.comments import StorageBackend from sphinx.websupport.comments.db import Base, Node, Comment, CommentVote,\ Session @@ -37,51 +38,89 @@ class SQLAlchemyStorage(StorageBackend): self.build_session.close() def add_comment(self, text, displayed, username, rating, time, - proposal, node, parent): + proposal, node_id, parent_id, moderator): session = Session() - if node: - node = session.query(Node).filter(Node.id == node).first() - parent = None - else: - node = None - parent = session.query(Comment).filter( - Comment.id == parent).first() - if node and proposal: + if node_id and proposal: differ = CombinedHtmlDiff() proposal_diff = differ.make_html(node.source, proposal) else: proposal_diff = None comment = Comment(text, displayed, username, rating, - time or datetime.now(), proposal, proposal_diff, - node, parent) + time or datetime.now(), proposal, proposal_diff) session.add(comment) + session.flush() + comment.set_path(node_id, parent_id) session.commit() comment = comment.serializable() session.close() return comment - def get_comments(self, node_id, user_id): + def get_comments(self, node_id, username, moderator): session = Session() - node = session.query(Node).filter(Node.id == node_id).first() - data = {'source': node.source, - 'comments': [comment.serializable(user_id) - for comment in node.comments]} + node = session.query(Node).filter(Node.id == node_id).one() session.close() - return data + comments = self._serializable_list(node_id, username, moderator) + return {'source': node.source, + 'comments': comments} + + def _serializable_list(self, node_id, username, moderator): + session = Session() + + if username: + # If a username is provided, create a subquery to retrieve all + # votes by this user. We will outerjoin with the comment query + # with this subquery so we have a user's voting information. + sq = session.query(CommentVote).\ + filter(CommentVote.username == username).subquery() + cvalias = aliased(CommentVote, sq) + q = session.query(Comment, cvalias.value).outerjoin(cvalias) + else: + q = session.query(Comment) + + # Filter out all comments not descending from this node. + q = q.filter(Comment.path.like(node_id + '.%')) + # Filter out non-displayed comments if this isn't a moderator. + if not moderator: + q = q.filter(Comment.displayed == True) + # Retrieve all results. Results must be ordered by Comment.path + # so that we can easily transform them from a flat list to a tree. + results = q.order_by(Comment.path).all() + session.close() + + # We now need to convert the flat list of results to a nested + # lists to form the comment tree. Results will by ordered by + # the materialized path. + comments = [] + list_stack = [comments] + for r in results: + comment, vote = r if username else (r, 0) + + inheritance_chain = comment.path.split('.')[1:] + + if len(inheritance_chain) == len(list_stack) + 1: + parent = list_stack[-1][-1] + list_stack.append(parent['children']) + elif len(inheritance_chain) < len(list_stack): + while len(inheritance_chain) < len(list_stack): + list_stack.pop() + + list_stack[-1].append(comment.serializable(vote=vote)) + + return comments - def process_vote(self, comment_id, user_id, value): + def process_vote(self, comment_id, username, value): session = Session() vote = session.query(CommentVote).filter( CommentVote.comment_id == comment_id).filter( - CommentVote.user_id == user_id).first() + CommentVote.username == username).first() comment = session.query(Comment).filter( Comment.id == comment_id).first() if vote is None: - vote = CommentVote(comment_id, user_id, value) + vote = CommentVote(comment_id, username, value) comment.rating += value else: comment.rating += value - vote.value diff --git a/tests/test_websupport.py b/tests/test_websupport.py index 53307fb4..f92dbcde 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -36,6 +36,7 @@ def clear_builddir(): def teardown_module(): + (test_root / 'generated').rmtree(True) clear_builddir() @@ -130,13 +131,61 @@ def test_whoosh(): @with_support() def test_comments(support): session = Session() - node = session.query(Node).first() - comment = support.add_comment('First test comment', node=str(node.id)) - support.add_comment('Child test comment', parent=str(comment['id'])) - data = support.get_comments(str(node.id)) + nodes = session.query(Node).all() + first_node = nodes[0] + second_node = nodes[1] + + # Create a displayed comment and a non displayed comment. + comment = support.add_comment('First test comment', + node_id=str(first_node.id)) + support.add_comment('Hidden comment', node_id=str(first_node.id), + displayed=False) + # Add a displayed and not displayed child to the displayed comment. + support.add_comment('Child test comment', parent_id=str(comment['id'])) + support.add_comment('Hidden child test comment', + parent_id=str(comment['id']), displayed=False) + # Add a comment to another node to make sure it isn't returned later. + support.add_comment('Second test comment', + node_id=str(second_node.id)) + + # Access the comments as a moderator. + data = support.get_comments(str(first_node.id), moderator=True) + comments = data['comments'] + children = comments[0]['children'] + assert len(comments) == 2 + assert comments[1]['text'] == 'Hidden comment' + assert len(children) == 2 + assert children[1]['text'] == 'Hidden child test comment' + + # Access the comments without being a moderator. + data = support.get_comments(str(first_node.id)) comments = data['comments'] children = comments[0]['children'] assert len(comments) == 1 assert comments[0]['text'] == 'First test comment' assert len(children) == 1 assert children[0]['text'] == 'Child test comment' + + def check_rating(val): + data = support.get_comments(str(first_node.id)) + comment = data['comments'][0] + assert comment['rating'] == val, '%s != %s' % (comment['rating'], val) + + support.process_vote(comment['id'], 'user_one', '1') + support.process_vote(comment['id'], 'user_two', '1') + support.process_vote(comment['id'], 'user_three', '1') + check_rating(3) + support.process_vote(comment['id'], 'user_one', '-1') + check_rating(1) + support.process_vote(comment['id'], 'user_one', '0') + check_rating(2) + + # Make sure a vote with value > 1 or < -1 can't be cast. + raises(ValueError, support.process_vote, comment['id'], 'user_one', '2') + raises(ValueError, support.process_vote, comment['id'], 'user_one', '-2') + + # Make sure past voting data is associated with comments when they are + # fetched. + data = support.get_comments(str(first_node.id), username='user_two') + comment = data['comments'][0] + assert comment['vote'] == 1, '%s != 1' % comment['vote'] -- cgit v1.2.1 From 308303a7271f4087ce39bcc2a547677fb0e9385e Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Wed, 4 Aug 2010 11:25:30 -0500 Subject: Separate search adapter tests from others --- tests/test_searchadapters.py | 82 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_websupport.py | 74 ++++----------------------------------- 2 files changed, 88 insertions(+), 68 deletions(-) create mode 100644 tests/test_searchadapters.py diff --git a/tests/test_searchadapters.py b/tests/test_searchadapters.py new file mode 100644 index 00000000..94f72cab --- /dev/null +++ b/tests/test_searchadapters.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +""" + test_searchadapters + ~~~~~~~~~~~~~~~~~~~ + + Test the Web Support Package search adapters. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import os +from StringIO import StringIO + +from util import * +from sphinx.websupport import WebSupport + + +def clear_builddir(): + (test_root / 'websupport').rmtree(True) + + +def teardown_module(): + (test_root / 'generated').rmtree(True) + clear_builddir() + + +def search_adapter_helper(adapter): + clear_builddir() + + settings = {'outdir': os.path.join(test_root, 'websupport'), + 'status': StringIO(), + 'warning': StringIO()} + settings.update({'srcdir': test_root, + 'search': adapter}) + support = WebSupport(**settings) + support.build() + + s = support.search + + # Test the adapters query method. A search for "Epigraph" should return + # one result. + results = s.query(u'Epigraph') + assert len(results) == 1, \ + '%s search adapter returned %s search result(s), should have been 1'\ + % (adapter, len(results)) + + # Make sure documents are properly updated by the search adapter. + s.init_indexing(changed=['markup']) + s.add_document(u'markup', u'title', u'SomeLongRandomWord') + s.finish_indexing() + # Now a search for "Epigraph" should return zero results. + results = s.query(u'Epigraph') + assert len(results) == 0, \ + '%s search adapter returned %s search result(s), should have been 0'\ + % (adapter, len(results)) + # A search for "SomeLongRandomWord" should return one result. + results = s.query(u'SomeLongRandomWord') + assert len(results) == 1, \ + '%s search adapter returned %s search result(s), should have been 1'\ + % (adapter, len(results)) + + +def test_xapian(): + # Don't run tests if xapian is not installed. + try: + import xapian + search_adapter_helper('xapian') + except ImportError: + sys.stderr.write('info: not running xapian tests, ' \ + 'xapian doesn\'t seem to be installed') + + +def test_whoosh(): + # Don't run tests if whoosh is not installed. + try: + import whoosh + search_adapter_helper('whoosh') + except ImportError: + sys.stderr.write('info: not running whoosh tests, ' \ + 'whoosh doesn\'t seem to be installed') + diff --git a/tests/test_websupport.py b/tests/test_websupport.py index f92dbcde..5e032604 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -9,13 +9,14 @@ :license: BSD, see LICENSE for details. """ -import os, sys +import os from StringIO import StringIO from sphinx.websupport import WebSupport from sphinx.websupport.errors import * from sphinx.websupport.comments.sqlalchemystorage import Session from sphinx.websupport.comments.db import Node +from util import * try: from functools import wraps @@ -23,26 +24,17 @@ except ImportError: # functools is new in 2.4 wraps = lambda f: (lambda w: w) -from util import * - - -default_settings = {'outdir': os.path.join(test_root, 'websupport'), - 'status': StringIO(), - 'warning': StringIO()} - - -def clear_builddir(): - (test_root / 'websupport').rmtree(True) - def teardown_module(): (test_root / 'generated').rmtree(True) - clear_builddir() + (test_root / 'websupport').rmtree(True) def with_support(*args, **kwargs): """Make a WebSupport object and pass it the test.""" - settings = default_settings.copy() + settings = {'outdir': os.path.join(test_root, 'websupport'), + 'status': StringIO(), + 'warning': StringIO()} settings.update(kwargs) def generator(func): @@ -74,60 +66,6 @@ def test_get_document(support): and contents['sidebar'] and contents['relbar'] -def search_adapter_helper(adapter): - clear_builddir() - - settings = default_settings.copy() - settings.update({'srcdir': test_root, - 'search': adapter}) - support = WebSupport(**settings) - support.build() - - s = support.search - - # Test the adapters query method. A search for "Epigraph" should return - # one result. - results = s.query(u'Epigraph') - assert len(results) == 1, \ - '%s search adapter returned %s search result(s), should have been 1'\ - % (adapter, len(results)) - - # Make sure documents are properly updated by the search adapter. - s.init_indexing(changed=['markup']) - s.add_document(u'markup', u'title', u'SomeLongRandomWord') - s.finish_indexing() - # Now a search for "Epigraph" should return zero results. - results = s.query(u'Epigraph') - assert len(results) == 0, \ - '%s search adapter returned %s search result(s), should have been 0'\ - % (adapter, len(results)) - # A search for "SomeLongRandomWord" should return one result. - results = s.query(u'SomeLongRandomWord') - assert len(results) == 1, \ - '%s search adapter returned %s search result(s), should have been 1'\ - % (adapter, len(results)) - - -def test_xapian(): - # Don't run tests if xapian is not installed. - try: - import xapian - search_adapter_helper('xapian') - except ImportError: - sys.stderr.write('info: not running xapian tests, ' \ - 'xapian doesn\'t seem to be installed') - - -def test_whoosh(): - # Don't run tests if whoosh is not installed. - try: - import whoosh - search_adapter_helper('whoosh') - except ImportError: - sys.stderr.write('info: not running whoosh tests, ' \ - 'whoosh doesn\'t seem to be installed') - - @with_support() def test_comments(support): session = Session() -- cgit v1.2.1 From da72f1ed90f9bb0c2f97887158e7acdcc2c115e6 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Wed, 4 Aug 2010 13:09:07 -0500 Subject: added more test coverage. --- sphinx/websupport/comments/differ.py | 4 +-- sphinx/websupport/comments/sqlalchemystorage.py | 1 + tests/test_searchadapters.py | 4 ++- tests/test_websupport.py | 48 +++++++++++++++++++++---- 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/sphinx/websupport/comments/differ.py b/sphinx/websupport/comments/differ.py index 78692234..2ecacea5 100644 --- a/sphinx/websupport/comments/differ.py +++ b/sphinx/websupport/comments/differ.py @@ -21,7 +21,7 @@ class CombinedHtmlDiff(object): next = next[2:] new_text = [] start = 0 - for match in highlight_regex.finditer(next): + for match in self.highlight_regex.finditer(next): new_text.append(text[start:match.start()]) new_text.append('<%s>' % tag) new_text.append(text[match.start():match.end()]) @@ -30,7 +30,7 @@ class CombinedHtmlDiff(object): new_text.append(text[start:]) return ''.join(new_text) - def _handle_line(line, next=None): + def _handle_line(self, line, next=None): prefix = line[0] text = line[2:] diff --git a/sphinx/websupport/comments/sqlalchemystorage.py b/sphinx/websupport/comments/sqlalchemystorage.py index 0e11c4a0..63db1550 100644 --- a/sphinx/websupport/comments/sqlalchemystorage.py +++ b/sphinx/websupport/comments/sqlalchemystorage.py @@ -42,6 +42,7 @@ class SQLAlchemyStorage(StorageBackend): session = Session() if node_id and proposal: + node = session.query(Node).filter(Node.id == node_id).one() differ = CombinedHtmlDiff() proposal_diff = differ.make_html(node.source, proposal) else: diff --git a/tests/test_searchadapters.py b/tests/test_searchadapters.py index 94f72cab..186b2e42 100644 --- a/tests/test_searchadapters.py +++ b/tests/test_searchadapters.py @@ -59,7 +59,9 @@ def search_adapter_helper(adapter): assert len(results) == 1, \ '%s search adapter returned %s search result(s), should have been 1'\ % (adapter, len(results)) - + # Make sure it works through the WebSupport API + html = support.get_search_results(u'SomeLongRandomWord') + def test_xapian(): # Don't run tests if xapian is not installed. diff --git a/tests/test_websupport.py b/tests/test_websupport.py index 5e032604..464c8c74 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -14,7 +14,9 @@ from StringIO import StringIO from sphinx.websupport import WebSupport from sphinx.websupport.errors import * -from sphinx.websupport.comments.sqlalchemystorage import Session +from sphinx.websupport.comments.differ import CombinedHtmlDiff +from sphinx.websupport.comments.sqlalchemystorage import Session, \ + SQLAlchemyStorage from sphinx.websupport.comments.db import Node from util import * @@ -25,6 +27,10 @@ except ImportError: wraps = lambda f: (lambda w: w) +default_settings = {'outdir': os.path.join(test_root, 'websupport'), + 'status': StringIO(), + 'warning': StringIO()} + def teardown_module(): (test_root / 'generated').rmtree(True) (test_root / 'websupport').rmtree(True) @@ -32,9 +38,7 @@ def teardown_module(): def with_support(*args, **kwargs): """Make a WebSupport object and pass it the test.""" - settings = {'outdir': os.path.join(test_root, 'websupport'), - 'status': StringIO(), - 'warning': StringIO()} + settings = default_settings.copy() settings.update(kwargs) def generator(func): @@ -104,8 +108,17 @@ def test_comments(support): assert len(children) == 1 assert children[0]['text'] == 'Child test comment' + +@with_support() +def test_voting(support): + session = Session() + nodes = session.query(Node).all() + node = nodes[0] + + comment = support.get_comments(str(node.id))['comments'][0] + def check_rating(val): - data = support.get_comments(str(first_node.id)) + data = support.get_comments(str(node.id)) comment = data['comments'][0] assert comment['rating'] == val, '%s != %s' % (comment['rating'], val) @@ -124,6 +137,29 @@ def test_comments(support): # Make sure past voting data is associated with comments when they are # fetched. - data = support.get_comments(str(first_node.id), username='user_two') + data = support.get_comments(str(node.id), username='user_two') comment = data['comments'][0] assert comment['vote'] == 1, '%s != 1' % comment['vote'] + +@with_support() +def test_proposals(support): + session = Session() + nodes = session.query(Node).all() + node = nodes[0] + + data = support.get_comments(str(node.id)) + + source = data['source'] + proposal = source[:5] + source[10:15] + 'asdf' + source[15:] + + comment = support.add_comment('Proposal comment', + node_id=str(node.id), + proposal=proposal) + +def test_differ(): + differ = CombinedHtmlDiff() + source = 'Lorem ipsum dolor sit amet,\nconsectetur adipisicing elit,\n' \ + 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' + prop = 'Lorem dolor sit amet,\nconsectetur nihil adipisicing elit,\n' \ + 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' + differ.make_html(source, prop) -- cgit v1.2.1 From c09ca5be9eea990307a6ba7ebdc60ee91594b284 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Wed, 4 Aug 2010 13:12:57 -0500 Subject: renamed get_comments get_data --- sphinx/websupport/__init__.py | 4 ++-- sphinx/websupport/comments/sqlalchemystorage.py | 2 +- tests/test_websupport.py | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 373622eb..8bc2a0b8 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -157,7 +157,7 @@ class WebSupport(object): document['title'] = 'Search Results' return document - def get_comments(self, node_id, username=None, moderator=False): + def get_data(self, node_id, username=None, moderator=False): """Get the comments and source associated with `node_id`. If `user_id` is given vote information will be included with the returned comments. The default CommentBackend returns dict with @@ -191,7 +191,7 @@ class WebSupport(object): :param node_id: the id of the node to get comments for. :param user_id: the id of the user viewing the comments. """ - return self.storage.get_comments(node_id, username, moderator) + return self.storage.get_data(node_id, username, moderator) def add_comment(self, text, node_id='', parent_id='', displayed=True, username=None, rating=0, time=None, proposal=None, diff --git a/sphinx/websupport/comments/sqlalchemystorage.py b/sphinx/websupport/comments/sqlalchemystorage.py index 63db1550..085913fd 100644 --- a/sphinx/websupport/comments/sqlalchemystorage.py +++ b/sphinx/websupport/comments/sqlalchemystorage.py @@ -58,7 +58,7 @@ class SQLAlchemyStorage(StorageBackend): session.close() return comment - def get_comments(self, node_id, username, moderator): + def get_data(self, node_id, username, moderator): session = Session() node = session.query(Node).filter(Node.id == node_id).one() session.close() diff --git a/tests/test_websupport.py b/tests/test_websupport.py index 464c8c74..8f701cd2 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -91,7 +91,7 @@ def test_comments(support): node_id=str(second_node.id)) # Access the comments as a moderator. - data = support.get_comments(str(first_node.id), moderator=True) + data = support.get_data(str(first_node.id), moderator=True) comments = data['comments'] children = comments[0]['children'] assert len(comments) == 2 @@ -100,7 +100,7 @@ def test_comments(support): assert children[1]['text'] == 'Hidden child test comment' # Access the comments without being a moderator. - data = support.get_comments(str(first_node.id)) + data = support.get_data(str(first_node.id)) comments = data['comments'] children = comments[0]['children'] assert len(comments) == 1 @@ -115,10 +115,10 @@ def test_voting(support): nodes = session.query(Node).all() node = nodes[0] - comment = support.get_comments(str(node.id))['comments'][0] + comment = support.get_data(str(node.id))['comments'][0] def check_rating(val): - data = support.get_comments(str(node.id)) + data = support.get_data(str(node.id)) comment = data['comments'][0] assert comment['rating'] == val, '%s != %s' % (comment['rating'], val) @@ -137,7 +137,7 @@ def test_voting(support): # Make sure past voting data is associated with comments when they are # fetched. - data = support.get_comments(str(node.id), username='user_two') + data = support.get_data(str(node.id), username='user_two') comment = data['comments'][0] assert comment['vote'] == 1, '%s != 1' % comment['vote'] @@ -147,7 +147,7 @@ def test_proposals(support): nodes = session.query(Node).all() node = nodes[0] - data = support.get_comments(str(node.id)) + data = support.get_data(str(node.id)) source = data['source'] proposal = source[:5] + source[10:15] + 'asdf' + source[15:] -- cgit v1.2.1 From 161a02c10b69b7434a22806dcf20281aa3d48755 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Wed, 4 Aug 2010 13:20:43 -0500 Subject: rename comments package storage --- sphinx/websupport/__init__.py | 4 ++-- tests/test_searchadapters.py | 2 +- tests/test_websupport.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 8bc2a0b8..4f469a4a 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -19,7 +19,7 @@ from jinja2 import Environment, FileSystemLoader from sphinx.application import Sphinx from sphinx.util.osutil import ensuredir from sphinx.websupport.search import BaseSearch, search_adapters -from sphinx.websupport.comments import StorageBackend +from sphinx.websupport.storage import StorageBackend from sphinx.websupport.errors import * class WebSupportApp(Sphinx): @@ -54,7 +54,7 @@ class WebSupport(object): else: # If a StorageBackend isn't provided, use the default # SQLAlchemy backend with an SQLite db. - from sphinx.websupport.comments.sqlalchemystorage \ + from sphinx.websupport.storage.sqlalchemystorage \ import SQLAlchemyStorage from sqlalchemy import create_engine db_path = path.join(self.outdir, 'db', 'websupport.db') diff --git a/tests/test_searchadapters.py b/tests/test_searchadapters.py index 186b2e42..c9525f75 100644 --- a/tests/test_searchadapters.py +++ b/tests/test_searchadapters.py @@ -9,7 +9,7 @@ :license: BSD, see LICENSE for details. """ -import os +import os, sys from StringIO import StringIO from util import * diff --git a/tests/test_websupport.py b/tests/test_websupport.py index 8f701cd2..bfa07226 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -14,10 +14,10 @@ from StringIO import StringIO from sphinx.websupport import WebSupport from sphinx.websupport.errors import * -from sphinx.websupport.comments.differ import CombinedHtmlDiff -from sphinx.websupport.comments.sqlalchemystorage import Session, \ +from sphinx.websupport.storage.differ import CombinedHtmlDiff +from sphinx.websupport.storage.sqlalchemystorage import Session, \ SQLAlchemyStorage -from sphinx.websupport.comments.db import Node +from sphinx.websupport.storage.db import Node from util import * try: -- cgit v1.2.1 From 1b9e6437091ea7a985caf565bb1c024731bfbe7b Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Wed, 4 Aug 2010 14:28:30 -0500 Subject: allow custom db uris for sqlalchemybackend --- sphinx/websupport/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 4f469a4a..b5cea021 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -32,7 +32,6 @@ class WebSupport(object): """The main API class for the web support package. All interactions with the web support package should occur through this class. """ - def __init__(self, srcdir='', outdir='', datadir='', search=None, storage=None, status=sys.stdout, warning=sys.stderr): self.srcdir = srcdir @@ -53,13 +52,14 @@ class WebSupport(object): self.storage = storage else: # If a StorageBackend isn't provided, use the default - # SQLAlchemy backend with an SQLite db. + # SQLAlchemy backend. from sphinx.websupport.storage.sqlalchemystorage \ import SQLAlchemyStorage from sqlalchemy import create_engine db_path = path.join(self.outdir, 'db', 'websupport.db') ensuredir(path.dirname(db_path)) - engine = create_engine('sqlite:///%s' % db_path) + uri = storage or 'sqlite:///%s' % db_path + engine = create_engine(uri) self.storage = SQLAlchemyStorage(engine) def _init_templating(self): -- cgit v1.2.1 From ad6dd82f3276ab0d8d2973c140330d854f13ff48 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Wed, 4 Aug 2010 16:06:10 -0500 Subject: added delete_comment method --- sphinx/websupport/__init__.py | 19 ++++++++++++++++-- sphinx/websupport/errors.py | 7 ++++++- tests/test_websupport.py | 46 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index b5cea021..03b9c8e8 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -85,7 +85,7 @@ class WebSupport(object): """Build the documentation. Places the data into the `outdir` directory. Use it like this:: - support = WebSupport(srcdir, outdir, search) + support = WebSupport(srcdir, outdir, search='xapian') support.build() This will read reStructured text files from `srcdir`. Then it @@ -109,7 +109,7 @@ class WebSupport(object): """Load and return a document from a pickle. The document will be a dict object which can be used to render a template:: - support = WebSupport(outdir=outdir) + support = WebSupport(datadir=datadir) support.get_document('index') In most cases `docname` will be taken from the request path and @@ -193,6 +193,21 @@ class WebSupport(object): """ return self.storage.get_data(node_id, username, moderator) + def delete_comment(self, comment_id, username='', moderator=False): + """Delete a comment. Doesn't actually delete the comment, but + instead replaces the username and text files with "[deleted]" so + as not to leave any comments orphaned. + + If `moderator` is True, the comment will always be deleted. If + `moderator` is False, the comment will only be deleted if the + `username` matches the `username` on the comment. + + :param comment_id: the id of the comment to delete. + :param username: the username requesting the deletion. + :param moderator: whether the requestor is a moderator. + """ + self.storage.delete_comment(comment_id, username, moderator) + def add_comment(self, text, node_id='', parent_id='', displayed=True, username=None, rating=0, time=None, proposal=None, moderator=False): diff --git a/sphinx/websupport/errors.py b/sphinx/websupport/errors.py index b1c47915..fbb75a93 100644 --- a/sphinx/websupport/errors.py +++ b/sphinx/websupport/errors.py @@ -9,7 +9,8 @@ :license: BSD, see LICENSE for details. """ -__all__ = ['DocumentNotFoundError', 'SrcdirNotSpecifiedError'] +__all__ = ['DocumentNotFoundError', 'SrcdirNotSpecifiedError', + 'UserNotAuthorizedError'] class DocumentNotFoundError(Exception): pass @@ -17,3 +18,7 @@ class DocumentNotFoundError(Exception): class SrcdirNotSpecifiedError(Exception): pass + + +class UserNotAuthorizedError(Exception): + pass diff --git a/tests/test_websupport.py b/tests/test_websupport.py index bfa07226..ca64ec2d 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -79,11 +79,13 @@ def test_comments(support): # Create a displayed comment and a non displayed comment. comment = support.add_comment('First test comment', - node_id=str(first_node.id)) + node_id=str(first_node.id), + username='user_one') support.add_comment('Hidden comment', node_id=str(first_node.id), displayed=False) # Add a displayed and not displayed child to the displayed comment. - support.add_comment('Child test comment', parent_id=str(comment['id'])) + support.add_comment('Child test comment', parent_id=str(comment['id']), + username='user_one') support.add_comment('Hidden child test comment', parent_id=str(comment['id']), displayed=False) # Add a comment to another node to make sure it isn't returned later. @@ -144,8 +146,7 @@ def test_voting(support): @with_support() def test_proposals(support): session = Session() - nodes = session.query(Node).all() - node = nodes[0] + node = session.query(Node).first() data = support.get_data(str(node.id)) @@ -156,6 +157,43 @@ def test_proposals(support): node_id=str(node.id), proposal=proposal) + +@with_support() +def test_user_delete_comments(support): + def get_comment(): + session = Session() + node = session.query(Node).first() + session.close() + return support.get_data(str(node.id))['comments'][0] + + comment = get_comment() + assert comment['username'] == 'user_one' + # Make sure other normal users can't delete someone elses comments. + raises(UserNotAuthorizedError, support.delete_comment, + comment['id'], username='user_two') + # Now delete the comment using the correct username. + support.delete_comment(comment['id'], username='user_one') + comment = get_comment() + assert comment['username'] == '[deleted]' + assert comment['text'] == '[deleted]' + + +@with_support() +def test_moderator_delete_comments(support): + def get_comment(): + session = Session() + node = session.query(Node).first() + session.close() + return support.get_data(str(node.id), moderator=True)['comments'][1] + + comment = get_comment() + support.delete_comment(comment['id'], username='user_two', + moderator=True) + comment = get_comment() + assert comment['username'] == '[deleted]' + assert comment['text'] == '[deleted]' + + def test_differ(): differ = CombinedHtmlDiff() source = 'Lorem ipsum dolor sit amet,\nconsectetur adipisicing elit,\n' \ -- cgit v1.2.1 From 16e0f128d237626c361eda00de115739b46b317b Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Thu, 5 Aug 2010 12:20:15 -0500 Subject: added update_username method --- sphinx/websupport/__init__.py | 13 +++++++++++++ tests/test_websupport.py | 22 ++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 03b9c8e8..f137ce2c 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -268,3 +268,16 @@ class WebSupport(object): if not -1 <= value <= 1: raise ValueError('vote value %s out of range (-1, 1)' % value) self.storage.process_vote(comment_id, username, value) + + def update_username(self, old_username, new_username): + """To remain decoupled from a webapp's authentication system, the + web support package stores a user's username with each of their + comments and votes. If the authentication system allows a user to + change their username, this can lead to stagnate data in the web + support system. To avoid this, each time a username is changed, this + method should be called. + + :param old_username: The original username. + :param new_username: The new username. + """ + self.storage.update_username(old_username, new_username) diff --git a/tests/test_websupport.py b/tests/test_websupport.py index ca64ec2d..d0956916 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -16,7 +16,7 @@ from sphinx.websupport import WebSupport from sphinx.websupport.errors import * from sphinx.websupport.storage.differ import CombinedHtmlDiff from sphinx.websupport.storage.sqlalchemystorage import Session, \ - SQLAlchemyStorage + SQLAlchemyStorage, Comment, CommentVote from sphinx.websupport.storage.db import Node from util import * @@ -90,7 +90,8 @@ def test_comments(support): parent_id=str(comment['id']), displayed=False) # Add a comment to another node to make sure it isn't returned later. support.add_comment('Second test comment', - node_id=str(second_node.id)) + node_id=str(second_node.id), + username='user_two') # Access the comments as a moderator. data = support.get_data(str(first_node.id), moderator=True) @@ -193,6 +194,23 @@ def test_moderator_delete_comments(support): assert comment['username'] == '[deleted]' assert comment['text'] == '[deleted]' +@with_support() +def test_update_username(support): + support.update_username('user_two', 'new_user_two') + session = Session() + comments = session.query(Comment).\ + filter(Comment.username == 'user_two').all() + assert len(comments) == 0 + votes = session.query(CommentVote).\ + filter(CommentVote.username == 'user_two') + assert len(comments) == 0 + comments = session.query(Comment).\ + filter(Comment.username == 'new_user_two').all() + assert len(comments) == 1 + votes = session.query(CommentVote).\ + filter(CommentVote.username == 'new_user_two') + assert len(comments) == 1 + def test_differ(): differ = CombinedHtmlDiff() -- cgit v1.2.1 From b25afec9eac16797ebdddf5eab5baaf32bfaa4f6 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Thu, 5 Aug 2010 14:13:50 -0500 Subject: Add comment moderation backend --- sphinx/websupport/__init__.py | 27 +++++++++++++++++++++++---- tests/test_websupport.py | 23 +++++++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index f137ce2c..e04e6899 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -33,10 +33,12 @@ class WebSupport(object): with the web support package should occur through this class. """ def __init__(self, srcdir='', outdir='', datadir='', search=None, - storage=None, status=sys.stdout, warning=sys.stderr): + storage=None, status=sys.stdout, warning=sys.stderr, + moderation_callback=None): self.srcdir = srcdir self.outdir = outdir or path.join(self.srcdir, '_build', 'websupport') + self.moderation_callback = moderation_callback self._init_templating() self.outdir = outdir or datadir @@ -236,9 +238,12 @@ class WebSupport(object): :param rating: the starting rating of the comment, defaults to 0. :param time: the time the comment was created, defaults to now. """ - return self.storage.add_comment(text, displayed, username, rating, - time, proposal, node_id, parent_id, - moderator) + comment = self.storage.add_comment(text, displayed, username, rating, + time, proposal, node_id, + parent_id, moderator) + if not displayed and self.moderation_callback: + self.moderation_callback(comment) + return comment def process_vote(self, comment_id, username, value): """Process a user's vote. The web support package relies @@ -281,3 +286,17 @@ class WebSupport(object): :param new_username: The new username. """ self.storage.update_username(old_username, new_username) + + def accept_comment(self, comment_id): + """Accept a comment that is pending moderation. + + :param comment_id: The id of the comment that was accepted. + """ + self.storage.accept_comment(comment_id) + + def reject_comment(self, comment_id): + """Reject a comment that is pending moderation. + + :param comment_id: The id of the comment that was accepted. + """ + self.storage.reject_comment(comment_id) diff --git a/tests/test_websupport.py b/tests/test_websupport.py index d0956916..e9c68cf6 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -194,6 +194,7 @@ def test_moderator_delete_comments(support): assert comment['username'] == '[deleted]' assert comment['text'] == '[deleted]' + @with_support() def test_update_username(support): support.update_username('user_two', 'new_user_two') @@ -212,6 +213,28 @@ def test_update_username(support): assert len(comments) == 1 +called = False +def moderation_callback(comment): + global called + called = True + + +@with_support(moderation_callback=moderation_callback) +def test_moderation(support): + accepted = support.add_comment('Accepted Comment', node_id=3, + displayed=False) + rejected = support.add_comment('Rejected comment', node_id=3, + displayed=False) + # Make sure the moderation_callback is called. + assert called == True + support.accept_comment(accepted['id']) + support.reject_comment(rejected['id']) + comments = support.get_data(3)['comments'] + assert len(comments) == 1 + comments = support.get_data(3, moderator=True)['comments'] + assert len(comments) == 1 + + def test_differ(): differ = CombinedHtmlDiff() source = 'Lorem ipsum dolor sit amet,\nconsectetur adipisicing elit,\n' \ -- cgit v1.2.1 From 3cac553d20cfcbe05a30585b135eb09c9f52c32e Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Thu, 5 Aug 2010 15:01:13 -0500 Subject: Ensure hidden comments can't be replied to --- sphinx/websupport/errors.py | 6 +++++- tests/test_websupport.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/sphinx/websupport/errors.py b/sphinx/websupport/errors.py index fbb75a93..e78abc21 100644 --- a/sphinx/websupport/errors.py +++ b/sphinx/websupport/errors.py @@ -10,7 +10,7 @@ """ __all__ = ['DocumentNotFoundError', 'SrcdirNotSpecifiedError', - 'UserNotAuthorizedError'] + 'UserNotAuthorizedError', 'CommentNotAllowedError'] class DocumentNotFoundError(Exception): pass @@ -22,3 +22,7 @@ class SrcdirNotSpecifiedError(Exception): class UserNotAuthorizedError(Exception): pass + + +class CommentNotAllowedError(Exception): + pass diff --git a/tests/test_websupport.py b/tests/test_websupport.py index e9c68cf6..2db29a2e 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -81,8 +81,14 @@ def test_comments(support): comment = support.add_comment('First test comment', node_id=str(first_node.id), username='user_one') - support.add_comment('Hidden comment', node_id=str(first_node.id), - displayed=False) + hidden_comment = support.add_comment('Hidden comment', + node_id=str(first_node.id), + displayed=False) + # Make sure that comments can't be added to a comment where + # displayed == False, since it could break the algorithm that + # converts a nodes comments to a tree. + raises(CommentNotAllowedError, support.add_comment, 'Not allowed', + parent_id=str(hidden_comment['id'])) # Add a displayed and not displayed child to the displayed comment. support.add_comment('Child test comment', parent_id=str(comment['id']), username='user_one') @@ -144,6 +150,7 @@ def test_voting(support): comment = data['comments'][0] assert comment['vote'] == 1, '%s != 1' % comment['vote'] + @with_support() def test_proposals(support): session = Session() -- cgit v1.2.1 From 7183be0f86c4be1914d388d944fea9641e152910 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Thu, 5 Aug 2010 19:29:24 -0500 Subject: added storage package --- sphinx/websupport/comments/__init__.py | 48 ------- sphinx/websupport/comments/db.py | 126 ----------------- sphinx/websupport/comments/differ.py | 66 --------- sphinx/websupport/comments/sqlalchemystorage.py | 131 ----------------- sphinx/websupport/storage/__init__.py | 48 +++++++ sphinx/websupport/storage/db.py | 126 +++++++++++++++++ sphinx/websupport/storage/differ.py | 66 +++++++++ sphinx/websupport/storage/sqlalchemystorage.py | 178 ++++++++++++++++++++++++ 8 files changed, 418 insertions(+), 371 deletions(-) delete mode 100644 sphinx/websupport/comments/__init__.py delete mode 100644 sphinx/websupport/comments/db.py delete mode 100644 sphinx/websupport/comments/differ.py delete mode 100644 sphinx/websupport/comments/sqlalchemystorage.py create mode 100644 sphinx/websupport/storage/__init__.py create mode 100644 sphinx/websupport/storage/db.py create mode 100644 sphinx/websupport/storage/differ.py create mode 100644 sphinx/websupport/storage/sqlalchemystorage.py diff --git a/sphinx/websupport/comments/__init__.py b/sphinx/websupport/comments/__init__.py deleted file mode 100644 index 10856dff..00000000 --- a/sphinx/websupport/comments/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.websupport.comments - ~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Comments for the websupport package. - - :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -""" - -class StorageBackend(object): - def pre_build(self): - """Called immediately before the build process begins. Use this - to prepare the StorageBackend for the addition of nodes. - """ - pass - - def add_node(self, document, line, source, treeloc): - """Add a node to the StorageBackend. - - `document` is the name of the document the node belongs to. - - `line` is the line in the source where the node begins. - - `source` is the source files name. - - `treeloc` is for future use. - """ - raise NotImplementedError() - - def post_build(self): - """Called after a build has completed. Use this to finalize the - addition of nodes if needed. - """ - pass - - def add_comment(self, text, displayed, username, rating, time, - proposal, node, parent): - """Called when a comment is being added.""" - raise NotImplementedError() - - def get_comments(self, parent_id, user_id, moderator): - """Called to retrieve all comments for a node.""" - raise NotImplementedError() - - def process_vote(self, comment_id, user_id, value): - raise NotImplementedError() diff --git a/sphinx/websupport/comments/db.py b/sphinx/websupport/comments/db.py deleted file mode 100644 index 91175ed3..00000000 --- a/sphinx/websupport/comments/db.py +++ /dev/null @@ -1,126 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.websupport.comments.db - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - SQLAlchemy table and mapper definitions used by the - :class:`sphinx.websupport.comments.SQLAlchemyStorage`. - - :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -""" - -from datetime import datetime - -from sqlalchemy import Column, Integer, Text, String, Boolean, ForeignKey,\ - DateTime -from sqlalchemy.schema import UniqueConstraint -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relation, sessionmaker - -Base = declarative_base() - -Session = sessionmaker() - -db_prefix = 'sphinx_' - -class Node(Base): - """Data about a Node in a doctree.""" - __tablename__ = db_prefix + 'nodes' - - id = Column(Integer, primary_key=True) - document = Column(String(256), nullable=False) - line = Column(Integer) - source = Column(Text, nullable=False) - - def __init__(self, document, line, source, treeloc): - self.document = document - self.line = line - self.source = source - self.treeloc = treeloc - -class Comment(Base): - __tablename__ = db_prefix + 'comments' - - id = Column(Integer, primary_key=True) - rating = Column(Integer, nullable=False) - time = Column(DateTime, nullable=False) - text = Column(Text, nullable=False) - displayed = Column(Boolean, index=True, default=False) - username = Column(String(64)) - proposal = Column(Text) - proposal_diff = Column(Text) - path = Column(String(256), index=True) - - #node_id = Column(Integer, ForeignKey(db_prefix + 'nodes.id')) - #node = relation(Node, backref='comments') - - def __init__(self, text, displayed, username, rating, time, - proposal, proposal_diff): - self.text = text - self.displayed = displayed - self.username = username - self.rating = rating - self.time = time - self.proposal = proposal - self.proposal_diff = proposal_diff - - def set_path(self, node_id, parent_id): - if node_id: - self.path = '%s.%s' % (node_id, self.id) - else: - session = Session() - parent_path = session.query(Comment.path).\ - filter(Comment.id == parent_id).one().path - session.close() - self.path = '%s.%s' % (parent_path, self.id) - - def serializable(self, vote=0): - delta = datetime.now() - self.time - - time = {'year': self.time.year, - 'month': self.time.month, - 'day': self.time.day, - 'hour': self.time.hour, - 'minute': self.time.minute, - 'second': self.time.second, - 'iso': self.time.isoformat(), - 'delta': self.pretty_delta(delta)} - - return {'text': self.text, - 'username': self.username or 'Anonymous', - 'id': self.id, - 'rating': self.rating, - 'age': delta.seconds, - 'time': time, - 'vote': vote or 0, - 'proposal_diff': self.proposal_diff, - 'children': []} - - def pretty_delta(self, delta): - days = delta.days - seconds = delta.seconds - hours = seconds / 3600 - minutes = seconds / 60 - - if days == 0: - dt = (minutes, 'minute') if hours == 0 else (hours, 'hour') - else: - dt = (days, 'day') - - return '%s %s ago' % dt if dt[0] == 1 else '%s %ss ago' % dt - -class CommentVote(Base): - __tablename__ = db_prefix + 'commentvote' - - username = Column(String(64), primary_key=True) - comment_id = Column(Integer, ForeignKey(db_prefix + 'comments.id'), - primary_key=True) - comment = relation(Comment, backref="votes") - # -1 if downvoted, +1 if upvoted, 0 if voted then unvoted. - value = Column(Integer, nullable=False) - - def __init__(self, comment_id, username, value): - self.comment_id = comment_id - self.username = username - self.value = value diff --git a/sphinx/websupport/comments/differ.py b/sphinx/websupport/comments/differ.py deleted file mode 100644 index 2ecacea5..00000000 --- a/sphinx/websupport/comments/differ.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.websupport.comments.differ - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - A differ for creating an HTML representations of proposal diffs - - :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -""" - -import re -from cgi import escape -from difflib import Differ - -class CombinedHtmlDiff(object): - - highlight_regex = re.compile(r'([\+\-\^]+)') - - def _highlight_text(self, text, next, tag): - next = next[2:] - new_text = [] - start = 0 - for match in self.highlight_regex.finditer(next): - new_text.append(text[start:match.start()]) - new_text.append('<%s>' % tag) - new_text.append(text[match.start():match.end()]) - new_text.append('</%s>' % tag) - start = match.end() - new_text.append(text[start:]) - return ''.join(new_text) - - def _handle_line(self, line, next=None): - prefix = line[0] - text = line[2:] - - if prefix == ' ': - return text - elif prefix == '?': - return '' - - if next[0] == '?': - tag = 'ins' if prefix == '+' else 'del' - text = self._highlight_text(text, next, tag) - css_class = 'prop_added' if prefix == '+' else 'prop_removed' - - return '<span class="%s">%s</span>\n' % (css_class, text.rstrip()) - - def make_html(self, source, proposal): - proposal = escape(proposal) - - differ = Differ() - diff = list(differ.compare(source.splitlines(1), - proposal.splitlines(1))) - html = [] - line = diff.pop(0) - next = diff.pop(0) - while True: - html.append(self._handle_line(line, next)) - line = next - try: - next = diff.pop(0) - except IndexError: - self._handle_line(line) - break - return ''.join(html) diff --git a/sphinx/websupport/comments/sqlalchemystorage.py b/sphinx/websupport/comments/sqlalchemystorage.py deleted file mode 100644 index 085913fd..00000000 --- a/sphinx/websupport/comments/sqlalchemystorage.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.websupport.comments.sqlalchemystorage - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - A SQLAlchemy storage backend. - - :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -""" - -from datetime import datetime - -from sqlalchemy.orm import aliased -from sphinx.websupport.comments import StorageBackend -from sphinx.websupport.comments.db import Base, Node, Comment, CommentVote,\ - Session -from sphinx.websupport.comments.differ import CombinedHtmlDiff - -class SQLAlchemyStorage(StorageBackend): - def __init__(self, engine): - self.engine = engine - Base.metadata.bind = engine - Base.metadata.create_all() - Session.configure(bind=engine) - - def pre_build(self): - self.build_session = Session() - - def add_node(self, document, line, source, treeloc): - node = Node(document, line, source, treeloc) - self.build_session.add(node) - self.build_session.flush() - return node.id - - def post_build(self): - self.build_session.commit() - self.build_session.close() - - def add_comment(self, text, displayed, username, rating, time, - proposal, node_id, parent_id, moderator): - session = Session() - - if node_id and proposal: - node = session.query(Node).filter(Node.id == node_id).one() - differ = CombinedHtmlDiff() - proposal_diff = differ.make_html(node.source, proposal) - else: - proposal_diff = None - - comment = Comment(text, displayed, username, rating, - time or datetime.now(), proposal, proposal_diff) - session.add(comment) - session.flush() - comment.set_path(node_id, parent_id) - session.commit() - comment = comment.serializable() - session.close() - return comment - - def get_data(self, node_id, username, moderator): - session = Session() - node = session.query(Node).filter(Node.id == node_id).one() - session.close() - comments = self._serializable_list(node_id, username, moderator) - return {'source': node.source, - 'comments': comments} - - def _serializable_list(self, node_id, username, moderator): - session = Session() - - if username: - # If a username is provided, create a subquery to retrieve all - # votes by this user. We will outerjoin with the comment query - # with this subquery so we have a user's voting information. - sq = session.query(CommentVote).\ - filter(CommentVote.username == username).subquery() - cvalias = aliased(CommentVote, sq) - q = session.query(Comment, cvalias.value).outerjoin(cvalias) - else: - q = session.query(Comment) - - # Filter out all comments not descending from this node. - q = q.filter(Comment.path.like(node_id + '.%')) - # Filter out non-displayed comments if this isn't a moderator. - if not moderator: - q = q.filter(Comment.displayed == True) - # Retrieve all results. Results must be ordered by Comment.path - # so that we can easily transform them from a flat list to a tree. - results = q.order_by(Comment.path).all() - session.close() - - # We now need to convert the flat list of results to a nested - # lists to form the comment tree. Results will by ordered by - # the materialized path. - comments = [] - list_stack = [comments] - for r in results: - comment, vote = r if username else (r, 0) - - inheritance_chain = comment.path.split('.')[1:] - - if len(inheritance_chain) == len(list_stack) + 1: - parent = list_stack[-1][-1] - list_stack.append(parent['children']) - elif len(inheritance_chain) < len(list_stack): - while len(inheritance_chain) < len(list_stack): - list_stack.pop() - - list_stack[-1].append(comment.serializable(vote=vote)) - - return comments - - def process_vote(self, comment_id, username, value): - session = Session() - vote = session.query(CommentVote).filter( - CommentVote.comment_id == comment_id).filter( - CommentVote.username == username).first() - - comment = session.query(Comment).filter( - Comment.id == comment_id).first() - - if vote is None: - vote = CommentVote(comment_id, username, value) - comment.rating += value - else: - comment.rating += value - vote.value - vote.value = value - session.add(vote) - session.commit() - session.close() diff --git a/sphinx/websupport/storage/__init__.py b/sphinx/websupport/storage/__init__.py new file mode 100644 index 00000000..6948c8c7 --- /dev/null +++ b/sphinx/websupport/storage/__init__.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +""" + sphinx.websupport.storage + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + Storage for the websupport package. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +class StorageBackend(object): + def pre_build(self): + """Called immediately before the build process begins. Use this + to prepare the StorageBackend for the addition of nodes. + """ + pass + + def add_node(self, document, line, source, treeloc): + """Add a node to the StorageBackend. + + `document` is the name of the document the node belongs to. + + `line` is the line in the source where the node begins. + + `source` is the source files name. + + `treeloc` is for future use. + """ + raise NotImplementedError() + + def post_build(self): + """Called after a build has completed. Use this to finalize the + addition of nodes if needed. + """ + pass + + def add_comment(self, text, displayed, username, rating, time, + proposal, node, parent): + """Called when a comment is being added.""" + raise NotImplementedError() + + def get_data(self, parent_id, user_id, moderator): + """Called to retrieve all comments for a node.""" + raise NotImplementedError() + + def process_vote(self, comment_id, user_id, value): + raise NotImplementedError() diff --git a/sphinx/websupport/storage/db.py b/sphinx/websupport/storage/db.py new file mode 100644 index 00000000..23b6a462 --- /dev/null +++ b/sphinx/websupport/storage/db.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +""" + sphinx.websupport.storage.db + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + SQLAlchemy table and mapper definitions used by the + :class:`sphinx.websupport.comments.SQLAlchemyStorage`. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from datetime import datetime + +from sqlalchemy import Column, Integer, Text, String, Boolean, ForeignKey,\ + DateTime +from sqlalchemy.schema import UniqueConstraint +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relation, sessionmaker + +Base = declarative_base() + +Session = sessionmaker() + +db_prefix = 'sphinx_' + +class Node(Base): + """Data about a Node in a doctree.""" + __tablename__ = db_prefix + 'nodes' + + id = Column(Integer, primary_key=True) + document = Column(String(256), nullable=False) + line = Column(Integer) + source = Column(Text, nullable=False) + + def __init__(self, document, line, source, treeloc): + self.document = document + self.line = line + self.source = source + self.treeloc = treeloc + +class Comment(Base): + __tablename__ = db_prefix + 'comments' + + id = Column(Integer, primary_key=True) + rating = Column(Integer, nullable=False) + time = Column(DateTime, nullable=False) + text = Column(Text, nullable=False) + displayed = Column(Boolean, index=True, default=False) + username = Column(String(64)) + proposal = Column(Text) + proposal_diff = Column(Text) + path = Column(String(256), index=True) + + #node_id = Column(Integer, ForeignKey(db_prefix + 'nodes.id')) + #node = relation(Node, backref='comments') + + def __init__(self, text, displayed, username, rating, time, + proposal, proposal_diff): + self.text = text + self.displayed = displayed + self.username = username + self.rating = rating + self.time = time + self.proposal = proposal + self.proposal_diff = proposal_diff + + def set_path(self, node_id, parent_id): + if node_id: + self.path = '%s.%s' % (node_id, self.id) + else: + session = Session() + parent_path = session.query(Comment.path).\ + filter(Comment.id == parent_id).one().path + session.close() + self.path = '%s.%s' % (parent_path, self.id) + + def serializable(self, vote=0): + delta = datetime.now() - self.time + + time = {'year': self.time.year, + 'month': self.time.month, + 'day': self.time.day, + 'hour': self.time.hour, + 'minute': self.time.minute, + 'second': self.time.second, + 'iso': self.time.isoformat(), + 'delta': self.pretty_delta(delta)} + + return {'text': self.text, + 'username': self.username or 'Anonymous', + 'id': self.id, + 'rating': self.rating, + 'age': delta.seconds, + 'time': time, + 'vote': vote or 0, + 'proposal_diff': self.proposal_diff, + 'children': []} + + def pretty_delta(self, delta): + days = delta.days + seconds = delta.seconds + hours = seconds / 3600 + minutes = seconds / 60 + + if days == 0: + dt = (minutes, 'minute') if hours == 0 else (hours, 'hour') + else: + dt = (days, 'day') + + return '%s %s ago' % dt if dt[0] == 1 else '%s %ss ago' % dt + +class CommentVote(Base): + __tablename__ = db_prefix + 'commentvote' + + username = Column(String(64), primary_key=True) + comment_id = Column(Integer, ForeignKey(db_prefix + 'comments.id'), + primary_key=True) + comment = relation(Comment, backref="votes") + # -1 if downvoted, +1 if upvoted, 0 if voted then unvoted. + value = Column(Integer, nullable=False) + + def __init__(self, comment_id, username, value): + self.comment_id = comment_id + self.username = username + self.value = value diff --git a/sphinx/websupport/storage/differ.py b/sphinx/websupport/storage/differ.py new file mode 100644 index 00000000..4e5660c5 --- /dev/null +++ b/sphinx/websupport/storage/differ.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +""" + sphinx.websupport.storage.differ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + A differ for creating an HTML representations of proposal diffs + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import re +from cgi import escape +from difflib import Differ + +class CombinedHtmlDiff(object): + + highlight_regex = re.compile(r'([\+\-\^]+)') + + def _highlight_text(self, text, next, tag): + next = next[2:] + new_text = [] + start = 0 + for match in self.highlight_regex.finditer(next): + new_text.append(text[start:match.start()]) + new_text.append('<%s>' % tag) + new_text.append(text[match.start():match.end()]) + new_text.append('</%s>' % tag) + start = match.end() + new_text.append(text[start:]) + return ''.join(new_text) + + def _handle_line(self, line, next=None): + prefix = line[0] + text = line[2:] + + if prefix == ' ': + return text + elif prefix == '?': + return '' + + if next[0] == '?': + tag = 'ins' if prefix == '+' else 'del' + text = self._highlight_text(text, next, tag) + css_class = 'prop_added' if prefix == '+' else 'prop_removed' + + return '<span class="%s">%s</span>\n' % (css_class, text.rstrip()) + + def make_html(self, source, proposal): + proposal = escape(proposal) + + differ = Differ() + diff = list(differ.compare(source.splitlines(1), + proposal.splitlines(1))) + html = [] + line = diff.pop(0) + next = diff.pop(0) + while True: + html.append(self._handle_line(line, next)) + line = next + try: + next = diff.pop(0) + except IndexError: + self._handle_line(line) + break + return ''.join(html) diff --git a/sphinx/websupport/storage/sqlalchemystorage.py b/sphinx/websupport/storage/sqlalchemystorage.py new file mode 100644 index 00000000..e96f38cf --- /dev/null +++ b/sphinx/websupport/storage/sqlalchemystorage.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +""" + sphinx.websupport.storage.sqlalchemystorage + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + An SQLAlchemy storage backend. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from datetime import datetime + +from sqlalchemy.orm import aliased +from sphinx.websupport.errors import * +from sphinx.websupport.storage import StorageBackend +from sphinx.websupport.storage.db import Base, Node, Comment, CommentVote,\ + Session +from sphinx.websupport.storage.differ import CombinedHtmlDiff + +class SQLAlchemyStorage(StorageBackend): + def __init__(self, engine): + self.engine = engine + Base.metadata.bind = engine + Base.metadata.create_all() + Session.configure(bind=engine) + + def pre_build(self): + self.build_session = Session() + + def add_node(self, document, line, source, treeloc): + node = Node(document, line, source, treeloc) + self.build_session.add(node) + self.build_session.flush() + return node.id + + def post_build(self): + self.build_session.commit() + self.build_session.close() + + def add_comment(self, text, displayed, username, rating, time, + proposal, node_id, parent_id, moderator): + session = Session() + + if node_id and proposal: + node = session.query(Node).filter(Node.id == node_id).one() + differ = CombinedHtmlDiff() + proposal_diff = differ.make_html(node.source, proposal) + elif parent_id: + parent = session.query(Comment.displayed).\ + filter(Comment.id == parent_id).one() + if not parent.displayed: + raise CommentNotAllowedError( + "Can't add child to a parent that is not displayed") + proposal_diff = None + else: + proposal_diff = None + + comment = Comment(text, displayed, username, rating, + time or datetime.now(), proposal, proposal_diff) + session.add(comment) + session.flush() + comment.set_path(node_id, parent_id) + session.commit() + comment = comment.serializable() + session.close() + return comment + + def delete_comment(self, comment_id, username, moderator): + session = Session() + comment = session.query(Comment).\ + filter(Comment.id == comment_id).one() + if moderator or comment.username == username: + comment.username = '[deleted]' + comment.text = '[deleted]' + session.commit() + session.close() + else: + session.close() + raise UserNotAuthorizedError() + + def get_data(self, node_id, username, moderator): + session = Session() + node = session.query(Node).filter(Node.id == node_id).one() + session.close() + comments = self._serializable_list(node_id, username, moderator) + return {'source': node.source, + 'comments': comments} + + def _serializable_list(self, node_id, username, moderator): + session = Session() + + if username: + # If a username is provided, create a subquery to retrieve all + # votes by this user. We will outerjoin with the comment query + # with this subquery so we have a user's voting information. + sq = session.query(CommentVote).\ + filter(CommentVote.username == username).subquery() + cvalias = aliased(CommentVote, sq) + q = session.query(Comment, cvalias.value).outerjoin(cvalias) + else: + q = session.query(Comment) + + # Filter out all comments not descending from this node. + q = q.filter(Comment.path.like(str(node_id) + '.%')) + # Filter out non-displayed comments if this isn't a moderator. + if not moderator: + q = q.filter(Comment.displayed == True) + # Retrieve all results. Results must be ordered by Comment.path + # so that we can easily transform them from a flat list to a tree. + results = q.order_by(Comment.path).all() + session.close() + + # We now need to convert the flat list of results to a nested + # lists to form the comment tree. Results will by ordered by + # the materialized path. + comments = [] + list_stack = [comments] + for r in results: + comment, vote = r if username else (r, 0) + + inheritance_chain = comment.path.split('.')[1:] + + if len(inheritance_chain) == len(list_stack) + 1: + parent = list_stack[-1][-1] + list_stack.append(parent['children']) + elif len(inheritance_chain) < len(list_stack): + while len(inheritance_chain) < len(list_stack): + list_stack.pop() + + list_stack[-1].append(comment.serializable(vote=vote)) + + return comments + + def process_vote(self, comment_id, username, value): + session = Session() + vote = session.query(CommentVote).filter( + CommentVote.comment_id == comment_id).filter( + CommentVote.username == username).first() + + comment = session.query(Comment).filter( + Comment.id == comment_id).first() + + if vote is None: + vote = CommentVote(comment_id, username, value) + comment.rating += value + else: + comment.rating += value - vote.value + vote.value = value + session.add(vote) + session.commit() + session.close() + + def update_username(self, old_username, new_username): + session = Session() + session.query(Comment).filter(Comment.username == old_username).\ + update({Comment.username: new_username}) + session.query(CommentVote).\ + filter(CommentVote.username == old_username).\ + update({CommentVote.username: new_username}) + session.commit() + session.close() + + def accept_comment(self, comment_id): + session = Session() + comment = session.query(Comment).\ + filter(Comment.id == comment_id).one() + comment.displayed = True + session.commit() + session.close() + + def reject_comment(self, comment_id): + session = Session() + comment = session.query(Comment).\ + filter(Comment.id == comment_id).one() + session.delete(comment) + session.commit() + session.close() -- cgit v1.2.1 From 5cbf0496bebbb9d0e941d8611d659a75ecf3ef66 Mon Sep 17 00:00:00 2001 From: Ali Afshar <aafshar@gmail.com> Date: Fri, 6 Aug 2010 11:35:48 +0100 Subject: Make the dot command part of the caching system for dot output generation --- sphinx/ext/graphviz.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index 106de7a6..257ff1b6 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.py @@ -93,6 +93,7 @@ def render_dot(self, code, options, format, prefix='graphviz'): Render graphviz code into a PNG or PDF output file. """ hashkey = code.encode('utf-8') + str(options) + \ + str(self.builder.config.graphviz_dot) + \ str(self.builder.config.graphviz_dot_args) fname = '%s-%s.%s' % (prefix, sha(hashkey).hexdigest(), format) if hasattr(self.builder, 'imgpath'): -- cgit v1.2.1 From efc67aed675f564b8acf4e8e5ea6a38f5f7aead6 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Fri, 6 Aug 2010 14:07:12 -0500 Subject: use static paths for static files and resources --- sphinx/builders/websupport.py | 37 +++++++++++++++++++++++++++++++------ sphinx/websupport/__init__.py | 17 ++++++++--------- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 40901eef..eeadfc23 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -11,6 +11,9 @@ import cPickle as pickle from os import path +import posixpath +import shutil +from docutils.io import StringOutput from sphinx.util.osutil import os_path, relative_uri, ensuredir, copyfile from sphinx.builders.html import StandaloneHTMLBuilder @@ -29,7 +32,21 @@ class WebSupportBuilder(StandaloneHTMLBuilder): def write_doc(self, docname, doctree): # The translator needs the docname to generate ids. self.cur_docname = docname - StandaloneHTMLBuilder.write_doc(self, docname, doctree) + destination = StringOutput(encoding='utf-8') + doctree.settings = self.docsettings + + self.secnumbers = self.env.toc_secnumbers.get(docname, {}) + self.imgpath = '/' + posixpath.join(self.app.staticdir, '_images') + self.post_process_images(doctree) + self.dlpath = '/' + posixpath.join(self.app.staticdir, '_downloads') + self.docwriter.write(doctree, destination) + self.docwriter.assemble_parts() + body = self.docwriter.parts['fragment'] + metatags = self.docwriter.clean_meta + + ctx = self.get_doc_context(docname, body, metatags) + self.index_page(docname, doctree, ctx.get('title', '')) + self.handle_page(docname, ctx, event_arg=doctree) def get_target_uri(self, docname, typ=None): return docname @@ -50,8 +67,9 @@ class WebSupportBuilder(StandaloneHTMLBuilder): baseuri=self.get_target_uri(pagename)): if not resource: otheruri = self.get_target_uri(otheruri) - uri = relative_uri(baseuri, otheruri) or '#' - return uri + return relative_uri(baseuri, otheruri) or '#' + else: + return '/' + posixpath.join(self.app.staticdir, otheruri) ctx['pathto'] = pathto ctx['hasdoc'] = lambda name: name in self.env.all_docs ctx['encoding'] = encoding = self.config.html_output_encoding @@ -74,7 +92,7 @@ class WebSupportBuilder(StandaloneHTMLBuilder): doc_ctx['relbar'] = template_module.relbar() if not outfilename: - outfilename = path.join(self.outdir, + outfilename = path.join(self.outdir, 'pickles', os_path(pagename) + self.out_suffix) ensuredir(path.dirname(outfilename)) @@ -87,10 +105,17 @@ class WebSupportBuilder(StandaloneHTMLBuilder): # if there is a source file, copy the source file for the # "show source" link if ctx.get('sourcename'): - source_name = path.join(self.outdir, '_sources', - os_path(ctx['sourcename'])) + source_name = path.join(self.outdir, self.app.staticdir, + '_sources', os_path(ctx['sourcename'])) ensuredir(path.dirname(source_name)) copyfile(self.env.doc2path(pagename), source_name) + def handle_finish(self): + StandaloneHTMLBuilder.handle_finish(self) + shutil.move(path.join(self.outdir, '_images'), + path.join(self.outdir, self.app.staticdir, '_images')) + shutil.move(path.join(self.outdir, '_static'), + path.join(self.outdir, self.app.staticdir, '_static')) + def dump_search_index(self): self.indexer.finish_indexing() diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index e04e6899..29a0b0ee 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -24,6 +24,7 @@ from sphinx.websupport.errors import * class WebSupportApp(Sphinx): def __init__(self, *args, **kwargs): + self.staticdir = kwargs.pop('staticdir', None) self.search = kwargs.pop('search', None) self.storage = kwargs.pop('storage', None) Sphinx.__init__(self, *args, **kwargs) @@ -34,18 +35,15 @@ class WebSupport(object): """ def __init__(self, srcdir='', outdir='', datadir='', search=None, storage=None, status=sys.stdout, warning=sys.stderr, - moderation_callback=None): + moderation_callback=None, staticdir='static'): self.srcdir = srcdir - self.outdir = outdir or path.join(self.srcdir, '_build', - 'websupport') - self.moderation_callback = moderation_callback - self._init_templating() - self.outdir = outdir or datadir - + self.staticdir = staticdir.strip('/') self.status = status self.warning = warning + self.moderation_callback = moderation_callback + self._init_templating() self._init_search(search) self._init_storage(storage) @@ -101,7 +99,8 @@ class WebSupport(object): app = WebSupportApp(self.srcdir, self.srcdir, self.outdir, doctreedir, 'websupport', search=self.search, status=self.status, - warning=self.warning, storage=self.storage) + warning=self.warning, storage=self.storage, + staticdir=self.staticdir) self.storage.pre_build() app.build() @@ -129,7 +128,7 @@ class WebSupport(object): :param docname: the name of the document to load. """ - infilename = path.join(self.outdir, docname + '.fpickle') + infilename = path.join(self.outdir, 'pickles', docname + '.fpickle') try: f = open(infilename, 'rb') -- cgit v1.2.1 From 01e8fb00e4f2f0b346377606ea46c2c38f8ff696 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Fri, 6 Aug 2010 15:07:59 -0500 Subject: moved _serializable_list into db.py --- sphinx/builders/websupport.py | 3 +- sphinx/websupport/storage/db.py | 53 ++++++++++++++++++++++++-- sphinx/websupport/storage/sqlalchemystorage.py | 48 +---------------------- 3 files changed, 51 insertions(+), 53 deletions(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index eeadfc23..095bd555 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -30,11 +30,10 @@ class WebSupportBuilder(StandaloneHTMLBuilder): self.translator_class = WebSupportTranslator def write_doc(self, docname, doctree): - # The translator needs the docname to generate ids. - self.cur_docname = docname destination = StringOutput(encoding='utf-8') doctree.settings = self.docsettings + self.cur_docname = docname self.secnumbers = self.env.toc_secnumbers.get(docname, {}) self.imgpath = '/' + posixpath.join(self.app.staticdir, '_images') self.post_process_images(doctree) diff --git a/sphinx/websupport/storage/db.py b/sphinx/websupport/storage/db.py index 23b6a462..568558a3 100644 --- a/sphinx/websupport/storage/db.py +++ b/sphinx/websupport/storage/db.py @@ -16,7 +16,7 @@ from sqlalchemy import Column, Integer, Text, String, Boolean, ForeignKey,\ DateTime from sqlalchemy.schema import UniqueConstraint from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relation, sessionmaker +from sqlalchemy.orm import relation, sessionmaker, aliased Base = declarative_base() @@ -33,6 +33,54 @@ class Node(Base): line = Column(Integer) source = Column(Text, nullable=False) + def nested_comments(self, username, moderator): + session = Session() + + if username: + # If a username is provided, create a subquery to retrieve all + # votes by this user. We will outerjoin with the comment query + # with this subquery so we have a user's voting information. + sq = session.query(CommentVote).\ + filter(CommentVote.username == username).subquery() + cvalias = aliased(CommentVote, sq) + q = session.query(Comment, cvalias.value).outerjoin(cvalias) + else: + q = session.query(Comment) + + # Filter out all comments not descending from this node. + q = q.filter(Comment.path.like(str(self.id) + '.%')) + # Filter out non-displayed comments if this isn't a moderator. + if not moderator: + q = q.filter(Comment.displayed == True) + # Retrieve all results. Results must be ordered by Comment.path + # so that we can easily transform them from a flat list to a tree. + results = q.order_by(Comment.path).all() + session.close() + + # We now need to convert the flat list of results to a nested + # lists to form the comment tree. Results will by ordered by + # the materialized path. + return self._nest_comments(results, username) + + def _nest_comments(self, results, username): + comments = [] + list_stack = [comments] + for r in results: + comment, vote = r if username else (r, 0) + + inheritance_chain = comment.path.split('.')[1:] + + if len(inheritance_chain) == len(list_stack) + 1: + parent = list_stack[-1][-1] + list_stack.append(parent['children']) + elif len(inheritance_chain) < len(list_stack): + while len(inheritance_chain) < len(list_stack): + list_stack.pop() + + list_stack[-1].append(comment.serializable(vote=vote)) + + return comments + def __init__(self, document, line, source, treeloc): self.document = document self.line = line @@ -52,9 +100,6 @@ class Comment(Base): proposal_diff = Column(Text) path = Column(String(256), index=True) - #node_id = Column(Integer, ForeignKey(db_prefix + 'nodes.id')) - #node = relation(Node, backref='comments') - def __init__(self, text, displayed, username, rating, time, proposal, proposal_diff): self.text = text diff --git a/sphinx/websupport/storage/sqlalchemystorage.py b/sphinx/websupport/storage/sqlalchemystorage.py index e96f38cf..02fa33b5 100644 --- a/sphinx/websupport/storage/sqlalchemystorage.py +++ b/sphinx/websupport/storage/sqlalchemystorage.py @@ -11,7 +11,6 @@ from datetime import datetime -from sqlalchemy.orm import aliased from sphinx.websupport.errors import * from sphinx.websupport.storage import StorageBackend from sphinx.websupport.storage.db import Base, Node, Comment, CommentVote,\ @@ -83,55 +82,10 @@ class SQLAlchemyStorage(StorageBackend): session = Session() node = session.query(Node).filter(Node.id == node_id).one() session.close() - comments = self._serializable_list(node_id, username, moderator) + comments = node.nested_comments(username, moderator) return {'source': node.source, 'comments': comments} - def _serializable_list(self, node_id, username, moderator): - session = Session() - - if username: - # If a username is provided, create a subquery to retrieve all - # votes by this user. We will outerjoin with the comment query - # with this subquery so we have a user's voting information. - sq = session.query(CommentVote).\ - filter(CommentVote.username == username).subquery() - cvalias = aliased(CommentVote, sq) - q = session.query(Comment, cvalias.value).outerjoin(cvalias) - else: - q = session.query(Comment) - - # Filter out all comments not descending from this node. - q = q.filter(Comment.path.like(str(node_id) + '.%')) - # Filter out non-displayed comments if this isn't a moderator. - if not moderator: - q = q.filter(Comment.displayed == True) - # Retrieve all results. Results must be ordered by Comment.path - # so that we can easily transform them from a flat list to a tree. - results = q.order_by(Comment.path).all() - session.close() - - # We now need to convert the flat list of results to a nested - # lists to form the comment tree. Results will by ordered by - # the materialized path. - comments = [] - list_stack = [comments] - for r in results: - comment, vote = r if username else (r, 0) - - inheritance_chain = comment.path.split('.')[1:] - - if len(inheritance_chain) == len(list_stack) + 1: - parent = list_stack[-1][-1] - list_stack.append(parent['children']) - elif len(inheritance_chain) < len(list_stack): - while len(inheritance_chain) < len(list_stack): - list_stack.pop() - - list_stack[-1].append(comment.serializable(vote=vote)) - - return comments - def process_vote(self, comment_id, username, value): session = Session() vote = session.query(CommentVote).filter( -- cgit v1.2.1 From ec06d8d19503f5d287c944fbc3da48535f442edc Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Fri, 6 Aug 2010 16:41:10 -0500 Subject: add node or parent id to serializable comment --- sphinx/websupport/storage/db.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sphinx/websupport/storage/db.py b/sphinx/websupport/storage/db.py index 568558a3..ed2b3b11 100644 --- a/sphinx/websupport/storage/db.py +++ b/sphinx/websupport/storage/db.py @@ -132,9 +132,15 @@ class Comment(Base): 'iso': self.time.isoformat(), 'delta': self.pretty_delta(delta)} + path = self.path.split('.') + node = path[0] if len(path) == 2 else None + parent = path[-2] if len(path) > 2 else None + return {'text': self.text, 'username': self.username or 'Anonymous', 'id': self.id, + 'node': node, + 'parent': parent, 'rating': self.rating, 'age': delta.seconds, 'time': time, -- cgit v1.2.1 From 4ff8aea0fbed57f5755bb921584cb7d382116fa8 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Fri, 6 Aug 2010 17:22:05 -0500 Subject: check for next in differ --- sphinx/websupport/storage/differ.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/websupport/storage/differ.py b/sphinx/websupport/storage/differ.py index 4e5660c5..c82ba742 100644 --- a/sphinx/websupport/storage/differ.py +++ b/sphinx/websupport/storage/differ.py @@ -39,7 +39,7 @@ class CombinedHtmlDiff(object): elif prefix == '?': return '' - if next[0] == '?': + if next is not None and next[0] == '?': tag = 'ins' if prefix == '+' else 'del' text = self._highlight_text(text, next, tag) css_class = 'prop_added' if prefix == '+' else 'prop_removed' -- cgit v1.2.1 From 0ab4d966ae6fd79249d3c61b80e34674b5c122b8 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Fri, 6 Aug 2010 21:13:06 -0500 Subject: add moderator kwarg to moderation methods. --- sphinx/websupport/__init__.py | 8 ++++++-- sphinx/websupport/storage/db.py | 1 + tests/test_websupport.py | 7 +++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 29a0b0ee..4812bb93 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -286,16 +286,20 @@ class WebSupport(object): """ self.storage.update_username(old_username, new_username) - def accept_comment(self, comment_id): + def accept_comment(self, comment_id, moderator=False): """Accept a comment that is pending moderation. :param comment_id: The id of the comment that was accepted. """ + if not moderator: + raise UserNotAuthorizedError() self.storage.accept_comment(comment_id) - def reject_comment(self, comment_id): + def reject_comment(self, comment_id, moderator=False): """Reject a comment that is pending moderation. :param comment_id: The id of the comment that was accepted. """ + if not moderator: + raise UserNotAuthorizedError() self.storage.reject_comment(comment_id) diff --git a/sphinx/websupport/storage/db.py b/sphinx/websupport/storage/db.py index ed2b3b11..12c1e1d5 100644 --- a/sphinx/websupport/storage/db.py +++ b/sphinx/websupport/storage/db.py @@ -142,6 +142,7 @@ class Comment(Base): 'node': node, 'parent': parent, 'rating': self.rating, + 'displayed': self.displayed, 'age': delta.seconds, 'time': time, 'vote': vote or 0, diff --git a/tests/test_websupport.py b/tests/test_websupport.py index 2db29a2e..37f0a679 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -234,8 +234,11 @@ def test_moderation(support): displayed=False) # Make sure the moderation_callback is called. assert called == True - support.accept_comment(accepted['id']) - support.reject_comment(rejected['id']) + # Make sure the user must be a moderator. + raises(UserNotAuthorizedError, support.accept_comment, accepted['id']) + raises(UserNotAuthorizedError, support.reject_comment, accepted['id']) + support.accept_comment(accepted['id'], moderator=True) + support.reject_comment(rejected['id'], moderator=True) comments = support.get_data(3)['comments'] assert len(comments) == 1 comments = support.get_data(3, moderator=True)['comments'] -- cgit v1.2.1 From fa6a290f8a4feeb7fa37ca4a78f01115f7c8fd0a Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 7 Aug 2010 11:49:38 -0500 Subject: add DOCUMENTATION_OPTIONS to context --- sphinx/builders/websupport.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 095bd555..70ce5d2b 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -11,6 +11,7 @@ import cPickle as pickle from os import path +from cgi import escape import posixpath import shutil from docutils.io import StringOutput @@ -81,7 +82,8 @@ class WebSupportBuilder(StandaloneHTMLBuilder): # Create a dict that will be pickled and used by webapps. doc_ctx = {'body': ctx.get('body', ''), - 'title': ctx.get('title', '')} + 'title': ctx.get('title', ''), + 'DOCUMENTATION_OPTIONS': self._make_doc_options(ctx)} # Partially render the html template to proved a more useful ctx. template = self.templates.environment.get_template(templatename) template_module = template.make_module(ctx) @@ -118,3 +120,17 @@ class WebSupportBuilder(StandaloneHTMLBuilder): def dump_search_index(self): self.indexer.finish_indexing() + + def _make_doc_options(self, ctx): + t = """ +var DOCUMENTATION_OPTIONS = { + URL_ROOT: '%s', + VERSION: '%s', + COLLAPSE_INDEX: false, + FILE_SUFFIX: '', + HAS_SOURCE: '%s' +};""" + return t % (ctx.get('url_root', ''), escape(ctx['release']), + str(ctx['has_source']).lower()) + + -- cgit v1.2.1 From 34711e79bf81127c44fda3b577e97fe4736c68fa Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 7 Aug 2010 14:10:49 -0500 Subject: add COMMENT_OPTIONS to context --- sphinx/websupport/__init__.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 4812bb93..de719e36 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -35,12 +35,14 @@ class WebSupport(object): """ def __init__(self, srcdir='', outdir='', datadir='', search=None, storage=None, status=sys.stdout, warning=sys.stderr, - moderation_callback=None, staticdir='static'): + moderation_callback=None, staticdir='static', + docroot=''): self.srcdir = srcdir self.outdir = outdir or datadir self.staticdir = staticdir.strip('/') self.status = status self.warning = warning + self.docroot = docroot.strip('/') self.moderation_callback = moderation_callback self._init_templating() @@ -106,7 +108,7 @@ class WebSupport(object): app.build() self.storage.post_build() - def get_document(self, docname): + def get_document(self, docname, username='', moderator=False): """Load and return a document from a pickle. The document will be a dict object which can be used to render a template:: @@ -137,8 +139,32 @@ class WebSupport(object): 'The document "%s" could not be found' % docname) document = pickle.load(f) + document['COMMENT_OPTIONS'] = self._make_comment_options(username, + moderator) return document + def _make_comment_options(self, username, moderator): + parts = ['var COMMENT_OPTIONS = {'] + if self.docroot is not '': + parts.append('addCommentURL: "/%s/%s",' % (self.docroot, + 'add_comment')) + parts.append('getCommentsURL: "/%s/%s",' % (self.docroot, + 'get_comments')) + parts.append('processVoteURL: "/%s/%s",' % (self.docroot, + 'process_vote')) + parts.append('acceptCommentURL: "/%s/%s",' % (self.docroot, + 'accept_comment')) + parts.append('rejectCommentURL: "/%s/%s",' % (self.docroot, + 'reject_comment')) + parts.append('deleteCommentURL: "/%s/%s",' % (self.docroot, + 'delete_comment')) + if username is not '': + parts.append('voting: true,') + parts.append('username: "%s",' % username) + parts.append('moderator: %s' % str(moderator).lower()) + parts.append('};') + return '\n'.join(parts) + def get_search_results(self, q): """Perform a search for the query `q`, and create a set of search results. Then render the search results as html and -- cgit v1.2.1 From bc4f53fcc1400787fb51d85f2697b6a84d9ce3b3 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 7 Aug 2010 17:04:45 -0500 Subject: updated docs --- doc/web/api.rst | 2 +- doc/web/quickstart.rst | 173 +++++++++++++------------------------------- doc/web/storagebackends.rst | 12 +-- 3 files changed, 58 insertions(+), 129 deletions(-) diff --git a/doc/web/api.rst b/doc/web/api.rst index 0b86309c..fcd0513e 100644 --- a/doc/web/api.rst +++ b/doc/web/api.rst @@ -22,7 +22,7 @@ Methods .. automethod:: sphinx.websupport.WebSupport.get_document -.. automethod:: sphinx.websupport.WebSupport.get_comments +.. automethod:: sphinx.websupport.WebSupport.get_data .. automethod:: sphinx.websupport.WebSupport.add_comment diff --git a/doc/web/quickstart.rst b/doc/web/quickstart.rst index 16c650c2..b0a60507 100644 --- a/doc/web/quickstart.rst +++ b/doc/web/quickstart.rst @@ -24,7 +24,15 @@ class and call it's :meth:`~sphinx.websupport.WebSupport.build` method:: This will read reStructuredText sources from `srcdir` and place the necessary data in `outdir`. This directory contains all the data needed to display documents, search through documents, and add comments to -documents. +documents. It will also contain a subdirectory named "static", which +contains static files. These files will be linked to by Sphinx documents, +and should be served from "/static". + +.. note:: + + If you wish to serve static files from a path other than "/static", you + can do so by providing the *staticdir* keyword argument when creating + the :class:`~sphinx.websupport.api.WebSupport` object. Integrating Sphinx Documents Into Your Webapp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -51,6 +59,8 @@ This will return a dictionary containing the following items: * **sidebar**: The sidebar of the document as HTML * **relbar**: A div containing links to related documents * **title**: The title of the document +* **DOCUMENTATION_OPTIONS**: Javascript containing documentation options +* **COMMENT_OPTIONS**: Javascript containing comment options This dict can then be used as context for templates. The goal is to be easy to integrate with your existing templating system. An example using @@ -64,6 +74,15 @@ easy to integrate with your existing templating system. An example using {{ document.title }} {%- endblock %} + {%- block js %} + <script type="text/javascript"> + {{ document.DOCUMENTATION_OPTIONS|safe }} + {{ document.COMMENT_OPTIONS|safe }} + </script> + {{ super() }} + <script type="text/javascript" src="/static/websupport.js"></script> + {%- endblock %} + {%- block relbar %} {{ document.relbar|safe }} {%- endblock %} @@ -76,18 +95,40 @@ easy to integrate with your existing templating system. An example using {{ document.sidebar|safe }} {%- endblock %} -Most likely you'll want to create one function that can handle all of -document requests. An example `Flask <http://flask.pocoo.org/>`_ function -that performs this is:: +Authentication +-------------- + +To use certain features such as voting it must be possible to authenticate +users. The details of the authentication are left to the your application. +Once a user has been authenticated you can pass the user's details to certain +:class:`~sphinx.websupport.WebSupport` methods using the *username* and +*moderator* keyword arguments. The web support package will store the +username with comments and votes. The only caveat is that if you allow users +to change their username, you must update the websupport package's data:: + + support.update_username(old_username, new_username) + +*username* should be a unique string which identifies a user, and *moderator* +should be a boolean representing whether the user has moderation +privilieges. The default value for *moderator* is *False*. + +An example `Flask <http://flask.pocoo.org/>`_ function that checks whether +a user is logged in, and the retrieves a document is:: @app.route('/<path:docname>') def doc(docname): - document = support.get_document(docname) + if g.user: + document = support.get_document(docname, g.user.name, + g.user.moderator) + else: + document = support.get_document(docname) return render_template('doc.html', document=document) -This captures the request path, and passes it directly to -:meth:`~sphinx.websupport.WebSupport.get_document`, which then retrieves -the correct document. +The first thing to notice is that the *docname* is just the request path. +If the user is authenticated then the username and moderation status are +passed along with the docname to +:meth:`~sphinx.websupport.WebSupport.get_document`. The web support package +will then add this data to the COMMENT_OPTIONS that are used in the template. .. note:: @@ -122,118 +163,6 @@ dict in the same format that Comments ~~~~~~~~ -The web support package provides a way to attach comments to some nodes -in your document. It marks these nodes by adding a class and id to these -nodes. A client side script can then locate these nodes, and manipulate -them to allow commenting. A `jQuery <http://jquery.com>`_ script is also -being developed that will be included when it's complete. For now you can -find the script here: `websupport.js <http://bit.ly/cyaRaF>`_. This script -will use AJAX for all communications with the server. You can create your -own script for the front end if this doesn't meet your needs. More -information on that can be found :ref:`here <websupportfrontend>`. - -Before loading this script in your page, you need to create a COMMENT_OPTIONS -object describing how the script should function. In the simplest case you -will just need tell the script whether the current user is allowed to vote. -Once this is done you can import the script as you would any other: - -.. sourcecode:: guess - - <script type="text/javascript"> - var COMMENT_OPTIONS = { - {%- if g.user %} - voting: true, - {%- endif %} - } - </script> - <script type="text/javascript" src="/static/websupport.js></script> - -You will then need to define some templates that the script uses to -display comments. The first template defines the layout for the popup -div used to display comments: - -.. sourcecode:: guess - - <script type="text/html" id="popup_template"> - <div class="popup_comment"> - <a id="comment_close" href="#">x</a> - <h1>Comments</h1> - <form method="post" id="comment_form" action="/docs/add_comment"> - <textarea name="comment"></textarea> - <input type="submit" value="add comment" id="comment_button" /> - <input type="hidden" name="parent" /> - <p class="sort_options"> - Sort by: - <a href="#" class="sort_option" id="rating">top</a> - <a href="#" class="sort_option" id="ascage">newest</a> - <a href="#" class="sort_option" id="age">oldest</a> - </p> - </form> - <h3 id="comment_notification">loading comments... <img src="/static/ajax-loader.gif" alt="" /></h3> - <ul id="comment_ul"></ul> - </div> - <div id="focuser"></div> - </script> - -The next template is an `li` that contains the form used to -reply to a comment: - -.. sourcecode:: guess - - <script type="text/html" id="reply_template"> - <li> - <div class="reply_div" id="rd<%id%>"> - <form id="rf<%id%>"> - <textarea name="comment"></textarea> - <input type="submit" value="add reply" /> - <input type="hidden" name="parent" value="c<%id%>" /> - </form> - </div> - </li> - </script> - -The final template contains HTML that will be used to display comments -in the comment tree: - -.. sourcecode:: guess - - <script type="text/html" id="comment_template"> - <div id="cd<%id%>" class="spxcdiv"> - <div class="vote"> - <div class="arrow"> - <a href="#" id="uv<%id%>" class="vote"> - <img src="<%upArrow%>" /> - </a> - <a href="#" id="uu<%id%>" class="un vote"> - <img src="<%upArrowPressed%>" /> - </a> - </div> - <div class="arrow"> - <a href="#" id="dv<%id%>" class="vote"> - <img src="<%downArrow%>" id="da<%id%>" /> - </a> - <a href="#" id="du<%id%>" class="un vote"> - <img src="<%downArrowPressed%>" /> - </a> - </div> - </div> - <div class="comment_content"> - <p class="tagline comment"> - <span class="user_id"><%username%></span> - <span class="rating"><%pretty_rating%></span> - <span class="delta"><%time.delta%></span> - </p> - <p class="comment_text comment"><%text%></p> - <p class="comment_opts comment"> - <a href="#" class="reply" id="rl<%id%>">reply</a> - <a href="#" class="close_reply" id="cr<%id%>">hide</a> - </p> - <ul class="children" id="cl<%id%>"></ul> - </div> - <div class="clearleft"></div> - </div> - </script> - Now that this is done it's time to define the functions that handle the AJAX calls from the script. You will need three functions. The first function is used to add a new comment, and will call the web support method @@ -248,13 +177,13 @@ function is used to add a new comment, and will call the web support method return jsonify(comment=comment) Then next function handles the retrieval of comments for a specific node, -and is aptly named :meth:`~sphinx.websupport.WebSupport.get_comments`:: +and is aptly named :meth:`~sphinx.websupport.WebSupport.get_data`:: @app.route('/docs/get_comments') def get_comments(): user_id = g.user.id if g.user else None parent_id = request.args.get('parent', '') - comments = support.get_comments(parent_id, user_id) + comments = support.get_data(parent_id, user_id) return jsonify(comments=comments) The final function that is needed will call diff --git a/doc/web/storagebackends.rst b/doc/web/storagebackends.rst index 87e1b478..4a10e109 100644 --- a/doc/web/storagebackends.rst +++ b/doc/web/storagebackends.rst @@ -1,6 +1,6 @@ .. _storagebackends: -.. currentmodule:: sphinx.websupport.comments +.. currentmodule:: sphinx.websupport.storage Storage Backends ================ @@ -8,12 +8,12 @@ Storage Backends StorageBackend Methods ~~~~~~~~~~~~~~~~~~~~~~ -.. automethod:: sphinx.websupport.comments.StorageBackend.pre_build +.. automethod:: sphinx.websupport.storage.StorageBackend.pre_build -.. automethod:: sphinx.websupport.comments.StorageBackend.add_node +.. automethod:: sphinx.websupport.storage.StorageBackend.add_node -.. automethod:: sphinx.websupport.comments.StorageBackend.post_build +.. automethod:: sphinx.websupport.storage.StorageBackend.post_build -.. automethod:: sphinx.websupport.comments.StorageBackend.add_comment +.. automethod:: sphinx.websupport.storage.StorageBackend.add_comment -.. automethod:: sphinx.websupport.comments.StorageBackend.get_comments +.. automethod:: sphinx.websupport.storage.StorageBackend.get_data -- cgit v1.2.1 From 233a75bac117d8da408ae38715b23b266e09ff63 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 7 Aug 2010 18:44:22 -0500 Subject: add css and js to context --- sphinx/builders/websupport.py | 47 +++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 70ce5d2b..bc15b8c9 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -83,7 +83,8 @@ class WebSupportBuilder(StandaloneHTMLBuilder): # Create a dict that will be pickled and used by webapps. doc_ctx = {'body': ctx.get('body', ''), 'title': ctx.get('title', ''), - 'DOCUMENTATION_OPTIONS': self._make_doc_options(ctx)} + 'css': self._make_css(ctx), + 'js': self._make_js(ctx)} # Partially render the html template to proved a more useful ctx. template = self.templates.environment.get_template(templatename) template_module = template.make_module(ctx) @@ -121,16 +122,36 @@ class WebSupportBuilder(StandaloneHTMLBuilder): def dump_search_index(self): self.indexer.finish_indexing() - def _make_doc_options(self, ctx): - t = """ -var DOCUMENTATION_OPTIONS = { - URL_ROOT: '%s', - VERSION: '%s', - COLLAPSE_INDEX: false, - FILE_SUFFIX: '', - HAS_SOURCE: '%s' -};""" - return t % (ctx.get('url_root', ''), escape(ctx['release']), - str(ctx['has_source']).lower()) - + def _make_css(self, ctx): + def make_link(file): + path = ctx['pathto'](file, 1) + return '<link rel="stylesheet" href="%s" type=text/css />' % path + + links = [make_link('_static/pygments.css')] + for file in ctx['css_files']: + links.append(make_link(file)) + return '\n'.join(links) + + def _make_js(self, ctx): + def make_script(file): + 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) -- cgit v1.2.1 From 07b62111369f9b8905ddef6ccc27e1a9b827802d Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 7 Aug 2010 18:59:11 -0500 Subject: moved websupport.js into sphinx --- sphinx/themes/basic/static/websupport.js | 751 +++++++++++++++++++++++++++++++ 1 file changed, 751 insertions(+) create mode 100644 sphinx/themes/basic/static/websupport.js diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js new file mode 100644 index 00000000..0e2b4430 --- /dev/null +++ b/sphinx/themes/basic/static/websupport.js @@ -0,0 +1,751 @@ +(function($) { + $.fn.autogrow = function(){ + return this.each(function(){ + var textarea = this; + + $.fn.autogrow.resize(textarea); + + $(textarea) + .focus(function() { + textarea.interval = setInterval(function() { + $.fn.autogrow.resize(textarea); + }, 500); + }) + .blur(function() { + clearInterval(textarea.interval); + }); + }); + }; + + $.fn.autogrow.resize = function(textarea) { + var lineHeight = parseInt($(textarea).css('line-height')); + var lines = textarea.value.split('\n'); + var columns = textarea.cols; + var lineCount = 0; + $.each(lines, function() { + lineCount += Math.ceil(this.length/columns) || 1; + }); + var height = lineHeight*(lineCount+1); + $(textarea).css('height', height); + }; +})(jQuery); + +(function($) { + var commentListEmpty, popup, comp; + + function init() { + initTemplates(); + initEvents(); + initComparator(); + }; + + function initEvents() { + $('a#comment_close').click(function(event) { + event.preventDefault(); + hide(); + }); + $('form#comment_form').submit(function(event) { + event.preventDefault(); + addComment($('form#comment_form')); + }); + $('.vote').live("click", function() { + handleVote($(this)); + return false; + }); + $('a.reply').live("click", function() { + openReply($(this).attr('id').substring(2)); + return false; + }); + $('a.close_reply').live("click", function() { + closeReply($(this).attr('id').substring(2)); + return false; + }); + $('a.sort_option').click(function(event) { + event.preventDefault(); + handleReSort($(this)); + }); + $('a.show_proposal').live("click", function() { + showProposal($(this).attr('id').substring(2)); + return false; + }); + $('a.hide_proposal').live("click", function() { + hideProposal($(this).attr('id').substring(2)); + return false; + }); + $('a.show_propose_change').live("click", function() { + showProposeChange($(this).attr('id').substring(2)); + return false; + }); + $('a.hide_propose_change').live("click", function() { + hideProposeChange($(this).attr('id').substring(2)); + return false; + }); + $('a.accept_comment').live("click", function() { + acceptComment($(this).attr('id').substring(2)); + return false; + }); + $('a.reject_comment').live("click", function() { + rejectComment($(this).attr('id').substring(2)); + return false; + }); + $('a.delete_comment').live("click", function() { + deleteComment($(this).attr('id').substring(2)); + return false; + }); + }; + + function initTemplates() { + // Create our popup div, the same div is recycled each time comments + // are displayed. + popup = $(renderTemplate(popupTemplate, opts)); + // Setup autogrow on the textareas + popup.find('textarea').autogrow(); + $('body').append(popup); + }; + + /** + * Create a comp function. If the user has preferences stored in + * the sortBy cookie, use those, otherwise use the default. + */ + function initComparator() { + var by = 'rating'; // Default to sort by rating. + // If the sortBy cookie is set, use that instead. + if (document.cookie.length > 0) { + var start = document.cookie.indexOf('sortBy='); + if (start != -1) { + start = start + 7; + var end = document.cookie.indexOf(";", start); + if (end == -1) + end = document.cookie.length; + by = unescape(document.cookie.substring(start, end)); + } + } + setComparator(by); + }; + + /** + * Show the comments popup window. + */ + function show(nodeId) { + var id = nodeId.substring(1); + + // Reset the main comment form, and set the value of the parent input. + $('form#comment_form') + .find('textarea,input') + .removeAttr('disabled').end() + .find('input[name="node"]') + .val(id).end() + .find('textarea[name="proposal"]') + .val('') + .hide(); + + // Position the popup and show it. + var clientWidth = document.documentElement.clientWidth; + var popupWidth = $('div.popup_comment').width(); + $('div#focuser').fadeIn('fast'); + $('div.popup_comment') + .css({ + 'top': 100+$(window).scrollTop(), + 'left': clientWidth/2-popupWidth/2, + 'position': 'absolute' + }) + .fadeIn('fast', function() { + getComments(id); + }); + }; + + /** + * Hide the comments popup window. + */ + function hide() { + $('div#focuser').fadeOut('fast'); + $('div.popup_comment').fadeOut('fast', function() { + $('ul#comment_ul').empty(); + $('h3#comment_notification').show(); + $('form#comment_form').find('textarea') + .val('').end() + .find('textarea, input') + .removeAttr('disabled'); + }); + }; + + /** + * Perform an ajax request to get comments for a node + * and insert the comments into the comments tree. + */ + function getComments(id) { + $.ajax({ + type: 'GET', + url: opts.getCommentsURL, + data: {node: id}, + success: function(data, textStatus, request) { + var ul = $('ul#comment_ul').hide(); + $('form#comment_form') + .find('textarea[name="proposal"]') + .data('source', data.source); + + if (data.comments.length == 0) { + ul.html('<li>No comments yet.</li>'); + commentListEmpty = true; + var speed = 100; + } + else { + // If there are comments, sort them and put them in the list. + var comments = sortComments(data.comments); + var speed = data.comments.length * 100; + appendComments(comments, ul); + commentListEmpty = false; + } + $('h3#comment_notification').slideUp(speed+200); + ul.slideDown(speed); + }, + error: function(request, textStatus, error) { + showError('Oops, there was a problem retrieving the comments.'); + }, + dataType: 'json' + }); + }; + + /** + * Add a comment via ajax and insert the comment into the comment tree. + */ + function addComment(form) { + // Disable the form that is being submitted. + form.find('textarea,input').attr('disabled', 'disabled'); + + // Send the comment to the server. + $.ajax({ + type: "POST", + url: opts.addCommentURL, + dataType: 'json', + data: {node: form.find('input[name="node"]').val(), + parent: form.find('input[name="parent"]').val(), + text: form.find('textarea[name="comment"]').val(), + proposal: form.find('textarea[name="proposal"]').val()}, + success: function(data, textStatus, error) { + // Reset the form. + form.find('textarea') + .val('') + .add(form.find('input')) + .removeAttr('disabled'); + if (commentListEmpty) { + $('ul#comment_ul').empty(); + commentListEmpty = false; + } + insertComment(data.comment); + }, + error: function(request, textStatus, error) { + form.find('textarea,input').removeAttr('disabled'); + showError('Oops, there was a problem adding the comment.'); + } + }); + }; + + /** + * Recursively append comments to the main comment list and children + * lists, creating the comment tree. + */ + function appendComments(comments, ul) { + $.each(comments, function() { + var div = createCommentDiv(this); + ul.append($('<li></li>').html(div)); + appendComments(this.children, div.find('ul.children')); + // To avoid stagnating data, don't store the comments children in data. + this.children = null; + div.data('comment', this); + }); + }; + + /** + * After adding a new comment, it must be inserted in the correct + * location in the comment tree. + */ + function insertComment(comment) { + var div = createCommentDiv(comment); + + // To avoid stagnating data, don't store the comments children in data. + comment.children = null; + div.data('comment', comment); + + if (comment.node != null) { + var ul = $('ul#comment_ul'); + var siblings = getChildren(ul); + } + else { + var ul = $('#cl' + comment.parent); + var siblings = getChildren(ul); + } + + var li = $('<li></li>'); + li.hide(); + + // Determine where in the parents children list to insert this comment. + for(i=0; i < siblings.length; i++) { + if (comp(comment, siblings[i]) <= 0) { + $('#cd' + siblings[i].id) + .parent() + .before(li.html(div)); + li.slideDown('fast'); + return; + } + } + + // If we get here, this comment rates lower than all the others, + // or it is the only comment in the list. + ul.append(li.html(div)); + li.slideDown('fast'); + }; + + function acceptComment(id) { + $.ajax({ + type: 'POST', + url: opts.acceptCommentURL, + data: {id: id}, + success: function(data, textStatus, request) { + $('#cm' + id).fadeOut('fast'); + }, + error: function(request, textStatus, error) { + showError("Oops, there was a problem accepting the comment."); + }, + }); + }; + + function rejectComment(id) { + $.ajax({ + type: 'POST', + url: opts.rejectCommentURL, + data: {id: id}, + success: function(data, textStatus, request) { + var div = $('#cd' + id); + div.slideUp('fast', function() { + div.remove(); + }); + }, + error: function(request, textStatus, error) { + showError("Oops, there was a problem rejecting the comment."); + }, + }); + }; + + function deleteComment(id) { + $.ajax({ + type: 'POST', + url: opts.deleteCommentURL, + data: {id: id}, + success: function(data, textStatus, request) { + var div = $('#cd' + id); + div + .find('span.user_id:first') + .text('[deleted]').end() + .find('p.comment_text:first') + .text('[deleted]').end() + .find('#cm' + id + ', #dc' + id + ', #ac' + id + ', #rc' + id + + ', #sp' + id + ', #hp' + id + ', #cr' + id + ', #rl' + id) + .remove(); + var comment = div.data('comment'); + comment.username = '[deleted]'; + comment.text = '[deleted]'; + div.data('comment', comment); + }, + error: function(request, textStatus, error) { + showError("Oops, there was a problem deleting the comment."); + }, + }); + }; + + function showProposal(id) { + $('#sp' + id).hide(); + $('#hp' + id).show(); + $('#pr' + id).slideDown('fast'); + }; + + function hideProposal(id) { + $('#hp' + id).hide(); + $('#sp' + id).show(); + $('#pr' + id).slideUp('fast'); + }; + + function showProposeChange(id) { + $('a.show_propose_change').hide(); + $('a.hide_propose_change').show(); + var textarea = $('textarea[name="proposal"]'); + textarea.val(textarea.data('source')); + $.fn.autogrow.resize(textarea[0]); + textarea.slideDown('fast'); + }; + + function hideProposeChange(id) { + $('a.hide_propose_change').hide(); + $('a.show_propose_change').show(); + var textarea = $('textarea[name="proposal"]'); + textarea.val(''); + textarea.slideUp('fast'); + }; + + /** + * Handle when the user clicks on a sort by link. + */ + function handleReSort(link) { + setComparator(link.attr('id')); + // Save/update the sortBy cookie. + var expiration = new Date(); + expiration.setDate(expiration.getDate() + 365); + document.cookie= 'sortBy=' + escape(link.attr('id')) + + ';expires=' + expiration.toUTCString(); + var comments = getChildren($('ul#comment_ul'), true); + comments = sortComments(comments); + + appendComments(comments, $('ul#comment_ul').empty()); + }; + + /** + * Function to process a vote when a user clicks an arrow. + */ + function handleVote(link) { + if (!opts.voting) { + showError("You'll need to login to vote."); + return; + } + + var id = link.attr('id'); + // If it is an unvote, the new vote value is 0, + // Otherwise it's 1 for an upvote, or -1 for a downvote. + if (id.charAt(1) == 'u') + var value = 0; + else + var value = id.charAt(0) == 'u' ? 1 : -1; + + // The data to be sent to the server. + var d = { + comment_id: id.substring(2), + value: value + }; + + // Swap the vote and unvote links. + link.hide(); + $('#' + id.charAt(0) + (id.charAt(1) == 'u' ? 'v' : 'u') + d.comment_id) + .show(); + + // The div the comment is displayed in. + var div = $('div#cd' + d.comment_id); + var data = div.data('comment'); + + // If this is not an unvote, and the other vote arrow has + // already been pressed, unpress it. + if ((d.value != 0) && (data.vote == d.value*-1)) { + $('#' + (d.value == 1 ? 'd' : 'u') + 'u' + d.comment_id) + .hide(); + $('#' + (d.value == 1 ? 'd' : 'u') + 'v' + d.comment_id) + .show(); + } + + // Update the comments rating in the local data. + data.rating += (data.vote == 0) ? d.value : (d.value - data.vote); + data.vote = d.value; + div.data('comment', data); + + // Change the rating text. + div.find('.rating:first') + .text(data.rating + ' point' + (data.rating == 1 ? '' : 's')); + + // Send the vote information to the server. + $.ajax({ + type: "POST", + url: opts.processVoteURL, + data: d, + error: function(request, textStatus, error) { + showError("Oops, there was a problem casting that vote."); + } + }); + }; + + /** + * Open a reply form used to reply to an existing comment. + */ + function openReply(id) { + // Swap out the reply link for the hide link + $('#rl' + id).hide(); + $('#cr' + id).show(); + + // Add the reply li to the children ul. + var div = $(renderTemplate(replyTemplate, {id: id})).hide(); + $('#cl' + id) + .prepend(div) + // Setup the submit handler for the reply form. + .find('#rf' + id) + .submit(function(event) { + event.preventDefault(); + addComment($('#rf' + id)); + closeReply(id); + }); + div.slideDown('fast'); + }; + + /** + * Close the reply form opened with openReply. + */ + function closeReply(id) { + // Remove the reply div from the DOM. + $('#rd' + id).slideUp('fast', function() { + $(this).remove(); + }); + + // Swap out the hide link for the reply link + $('#cr' + id).hide(); + $('#rl' + id).show(); + }; + + /** + * Recursively sort a tree of comments using the comp comparator. + */ + function sortComments(comments) { + comments.sort(comp); + $.each(comments, function() { + this.children = sortComments(this.children); + }); + return comments; + }; + + /** + * Set comp, which is a comparator function used for sorting and + * inserting comments into the list. + */ + function setComparator(by) { + // If the first three letters are "asc", sort in ascending order + // and remove the prefix. + if (by.substring(0,3) == 'asc') { + var i = by.substring(3); + comp = function(a, b) { return a[i] - b[i]; } + } + // Otherwise sort in descending order. + else + comp = function(a, b) { return b[by] - a[by]; } + + // Reset link styles and format the selected sort option. + $('a.sel').attr('href', '#').removeClass('sel'); + $('#' + by).removeAttr('href').addClass('sel'); + }; + + /** + * Get the children comments from a ul. If recursive is true, + * recursively include childrens' children. + */ + function getChildren(ul, recursive) { + var children = []; + ul.children().children("[id^='cd']") + .each(function() { + var comment = $(this).data('comment'); + if (recursive) { + comment.children = + getChildren($(this).find('#cl' + comment.id), true); + } + children.push(comment); + }); + return children; + }; + + /** + * Create a div to display a comment in. + */ + function createCommentDiv(comment) { + // Prettify the comment rating. + comment.pretty_rating = comment.rating + ' point' + + (comment.rating == 1 ? '' : 's'); + // Create a div for this comment. + var context = $.extend({}, opts, comment); + var div = $(renderTemplate(commentTemplate, context)); + + // If the user has voted on this comment, highlight the correct arrow. + if (comment.vote) { + var direction = (comment.vote == 1) ? 'u' : 'd'; + div.find('#' + direction + 'v' + comment.id).hide(); + div.find('#' + direction + 'u' + comment.id).show(); + } + + if (comment.text != '[deleted]') { + div.find('a.reply').show(); + if (comment.proposal_diff) { + div.find('#sp' + comment.id).show(); + } + if (opts.moderator && !comment.displayed) { + div.find('#cm' + comment.id).show(); + } + if (opts.moderator || (opts.username == comment.username)) { + div.find('#dc' + comment.id).show(); + } + } + + return div; + } + + /** + * A simple template renderer. Placeholders such as <%id%> are replaced + * by context['id']. Items are always escaped. + */ + function renderTemplate(template, context) { + var esc = $('<span></span>'); + + function handle(ph, escape) { + var cur = context; + $.each(ph.split('.'), function() { + cur = cur[this]; + }); + return escape ? esc.text(cur || "").html() : cur; + } + + return template.replace(/<([%#])([\w\.]*)\1>/g, function(){ + return handle(arguments[2], arguments[1] == '%' ? true : false); + }); + }; + + function showError(message) { + $('<div class="popup_error">' + + '<h1>' + message + '</h1>' + + '</div>') + .appendTo('body') + .fadeIn("slow") + .delay(2000) + .fadeOut("slow"); + }; + + /** + * Add a link the user uses to open the comments popup. + */ + $.fn.comment = function() { + return this.each(function() { + $(this).append( + $('<a href="#" class="sphinx_comment"></a>') + .html(opts.commentHTML) + .click(function(event) { + event.preventDefault(); + show($(this).parent().attr('id')); + })); + }); + }; + + var replyTemplate = ' <li>\ + <div class="reply_div" id="rd<%id%>">\ + <form id="rf<%id%>">\ + <textarea name="comment"></textarea>\ + <input type="submit" value="add reply" />\ + <input type="hidden" name="parent" value="<%id%>" />\ + <input type="hidden" name="node" value="" />\ + </form>\ + </div>\ + </li>'; + + var commentTemplate = ' <div id="cd<%id%>" class="spxcdiv">\ + <div class="vote">\ + <div class="arrow">\ + <a href="#" id="uv<%id%>" class="vote">\ + <img src="<%upArrow%>" />\ + </a>\ + <a href="#" id="uu<%id%>" class="un vote">\ + <img src="<%upArrowPressed%>" />\ + </a>\ + </div>\ + <div class="arrow">\ + <a href="#" id="dv<%id%>" class="vote">\ + <img src="<%downArrow%>" id="da<%id%>" />\ + </a>\ + <a href="#" id="du<%id%>" class="un vote">\ + <img src="<%downArrowPressed%>" />\ + </a>\ + </div>\ + </div>\ + <div class="comment_content">\ + <p class="tagline comment">\ + <span class="user_id"><%username%></span>\ + <span class="rating"><%pretty_rating%></span>\ + <span class="delta"><%time.delta%></span>\ + </p>\ + <p class="comment_text comment"><%text%></p>\ + <p class="comment_opts comment">\ + <a href="#" class="reply hidden" id="rl<%id%>">reply ▹</a>\ + <a href="#" class="close_reply" id="cr<%id%>">reply ▿</a>\ + <a href="#" id="sp<%id%>" class="show_proposal">\ + proposal ▹\ + </a>\ + <a href="#" id="hp<%id%>" class="hide_proposal">\ + proposal ▿\ + </a>\ + <a href="#" id="dc<%id%>" class="delete_comment hidden">\ + delete\ + </a>\ + <span id="cm<%id%>" class="moderation hidden">\ + <a href="#" id="ac<%id%>" class="accept_comment">accept</a>\ + <a href="#" id="rc<%id%>" class="reject_comment">reject</a>\ + </span>\ + </p>\ + <pre class="proposal" id="pr<%id%>">\ +<#proposal_diff#>\ + </pre>\ + <ul class="children" id="cl<%id%>"></ul>\ + </div>\ + <div class="clearleft"></div>\ + </div>'; + + var popupTemplate = ' <div class="popup_comment">\ + <a id="comment_close" href="#">x</a>\ + <h1>Comments</h1>\ + <form method="post" id="comment_form" action="/docs/add_comment">\ + <textarea name="comment" cols="80"></textarea>\ + <p class="propose_button">\ + <a href="#" class="show_propose_change">\ + Propose a change ▹\ + </a>\ + <a href="#" class="hide_propose_change">\ + Propose a change ▿\ + </a>\ + </p>\ + <textarea name="proposal" cols="80" spellcheck="false"></textarea>\ + <input type="submit" value="add comment" id="comment_button" />\ + <input type="hidden" name="node" />\ + <input type="hidden" name="parent" value="" />\ + <p class="sort_options">\ + Sort by:\ + <a href="#" class="sort_option" id="rating">top</a>\ + <a href="#" class="sort_option" id="ascage">newest</a>\ + <a href="#" class="sort_option" id="age">oldest</a>\ + </p>\ + </form>\ + <h3 id="comment_notification">loading comments... <img src="/static/ajax-loader.gif" alt="" /></h3>\ + <ul id="comment_ul"></ul>\ + </div>\ + <div id="focuser"></div>'; + + + var opts = jQuery.extend({ + processVoteURL: '/process_vote', + addCommentURL: '/add_comment', + getCommentsURL: '/get_comments', + acceptCommentURL: '/accept_comment', + rejectCommentURL: '/reject_comment', + rejectCommentURL: '/delete_comment', + commentHTML: '<img src="/static/comment.png" alt="comment" />', + upArrow: '/static/up.png', + downArrow: '/static/down.png', + upArrowPressed: '/static/up-pressed.png', + downArrowPressed: '/static/down-pressed.png', + voting: false, + moderator: false + }, COMMENT_OPTIONS); + + $(document).ready(function() { + init(); + }); +})(jQuery); + +$(document).ready(function() { + $('.spxcmt').comment(); + + /** Highlight search words in search results. */ + $("div.context").each(function() { + var params = $.getQueryParameters(); + var terms = (params.q) ? params.q[0].split(/\s+/) : []; + var result = $(this); + $.each(terms, function() { + result.highlightText(this.toLowerCase(), 'highlighted'); + }); + }); +}); \ No newline at end of file -- cgit v1.2.1 From dde46f1328ff2b8d950b672720d112eee45172cd Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 7 Aug 2010 19:06:28 -0500 Subject: update get_document to add comment options to js --- sphinx/websupport/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index de719e36..34392813 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -139,12 +139,13 @@ class WebSupport(object): 'The document "%s" could not be found' % docname) document = pickle.load(f) - document['COMMENT_OPTIONS'] = self._make_comment_options(username, - moderator) + comment_opts = self._make_comment_options(username, moderator) + document['js'] = comment_opts + '\n' + document['js'] return document def _make_comment_options(self, username, moderator): - parts = ['var COMMENT_OPTIONS = {'] + parts = ['<script type="text/javascript">', + 'var COMMENT_OPTIONS = {'] if self.docroot is not '': parts.append('addCommentURL: "/%s/%s",' % (self.docroot, 'add_comment')) @@ -163,6 +164,7 @@ class WebSupport(object): parts.append('username: "%s",' % username) parts.append('moderator: %s' % str(moderator).lower()) parts.append('};') + parts.append('</script>') return '\n'.join(parts) def get_search_results(self, q): -- cgit v1.2.1 From 6e3d13e6db55a26f56002c959215f74aaba770b7 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 7 Aug 2010 20:21:53 -0500 Subject: an even nicer build directory --- sphinx/builders/websupport.py | 8 +++++--- sphinx/websupport/__init__.py | 17 ++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index bc15b8c9..cdd0fb92 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -107,7 +107,7 @@ class WebSupportBuilder(StandaloneHTMLBuilder): # if there is a source file, copy the source file for the # "show source" link if ctx.get('sourcename'): - source_name = path.join(self.outdir, self.app.staticdir, + source_name = path.join(self.app.builddir, self.app.staticdir, '_sources', os_path(ctx['sourcename'])) ensuredir(path.dirname(source_name)) copyfile(self.env.doc2path(pagename), source_name) @@ -115,9 +115,11 @@ class WebSupportBuilder(StandaloneHTMLBuilder): def handle_finish(self): StandaloneHTMLBuilder.handle_finish(self) shutil.move(path.join(self.outdir, '_images'), - path.join(self.outdir, self.app.staticdir, '_images')) + path.join(self.app.builddir, self.app.staticdir, + '_images')) shutil.move(path.join(self.outdir, '_static'), - path.join(self.outdir, self.app.staticdir, '_static')) + path.join(self.app.builddir, self.app.staticdir, + '_static')) def dump_search_index(self): self.indexer.finish_indexing() diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 34392813..9bd6cbda 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -25,6 +25,7 @@ from sphinx.websupport.errors import * class WebSupportApp(Sphinx): def __init__(self, *args, **kwargs): self.staticdir = kwargs.pop('staticdir', None) + self.builddir = kwargs.pop('builddir', None) self.search = kwargs.pop('search', None) self.storage = kwargs.pop('storage', None) Sphinx.__init__(self, *args, **kwargs) @@ -33,16 +34,18 @@ class WebSupport(object): """The main API class for the web support package. All interactions with the web support package should occur through this class. """ - def __init__(self, srcdir='', outdir='', datadir='', search=None, + def __init__(self, srcdir='', builddir='', datadir='', search=None, storage=None, status=sys.stdout, warning=sys.stderr, moderation_callback=None, staticdir='static', docroot=''): self.srcdir = srcdir - self.outdir = outdir or datadir + self.builddir = builddir + self.outdir = path.join(builddir, 'data') + self.datadir = datadir or self.outdir self.staticdir = staticdir.strip('/') + self.docroot = docroot.strip('/') self.status = status self.warning = warning - self.docroot = docroot.strip('/') self.moderation_callback = moderation_callback self._init_templating() @@ -58,7 +61,7 @@ class WebSupport(object): from sphinx.websupport.storage.sqlalchemystorage \ import SQLAlchemyStorage from sqlalchemy import create_engine - db_path = path.join(self.outdir, 'db', 'websupport.db') + db_path = path.join(self.datadir, 'db', 'websupport.db') ensuredir(path.dirname(db_path)) uri = storage or 'sqlite:///%s' % db_path engine = create_engine(uri) @@ -78,7 +81,7 @@ class WebSupport(object): mod, cls = search_adapters[search or 'null'] mod = 'sphinx.websupport.search.' + mod SearchClass = getattr(__import__(mod, None, None, [cls]), cls) - search_path = path.join(self.outdir, 'search') + search_path = path.join(self.datadir, 'search') self.search = SearchClass(search_path) self.results_template = \ self.template_env.get_template('searchresults.html') @@ -102,7 +105,7 @@ class WebSupport(object): self.outdir, doctreedir, 'websupport', search=self.search, status=self.status, warning=self.warning, storage=self.storage, - staticdir=self.staticdir) + staticdir=self.staticdir, builddir=self.builddir) self.storage.pre_build() app.build() @@ -130,7 +133,7 @@ class WebSupport(object): :param docname: the name of the document to load. """ - infilename = path.join(self.outdir, 'pickles', docname + '.fpickle') + infilename = path.join(self.datadir, 'pickles', docname + '.fpickle') try: f = open(infilename, 'rb') -- cgit v1.2.1 From f2a0fa5cfe5d5409d0167790015ea1cebb77abfa Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 7 Aug 2010 21:08:43 -0500 Subject: updated tests to use builddir instead of outdir --- tests/test_searchadapters.py | 2 +- tests/test_websupport.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_searchadapters.py b/tests/test_searchadapters.py index c9525f75..cb6c6e96 100644 --- a/tests/test_searchadapters.py +++ b/tests/test_searchadapters.py @@ -28,7 +28,7 @@ def teardown_module(): def search_adapter_helper(adapter): clear_builddir() - settings = {'outdir': os.path.join(test_root, 'websupport'), + settings = {'builddir': os.path.join(test_root, 'websupport'), 'status': StringIO(), 'warning': StringIO()} settings.update({'srcdir': test_root, diff --git a/tests/test_websupport.py b/tests/test_websupport.py index 37f0a679..27a14e36 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -27,7 +27,7 @@ except ImportError: wraps = lambda f: (lambda w: w) -default_settings = {'outdir': os.path.join(test_root, 'websupport'), +default_settings = {'builddir': os.path.join(test_root, 'websupport'), 'status': StringIO(), 'warning': StringIO()} -- cgit v1.2.1 From 5db8b67f6a2d3c9b94e4d0dc355175ea4453d9e9 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sun, 8 Aug 2010 11:10:56 -0500 Subject: added static images used for web support --- sphinx/themes/basic/static/ajax-loader.gif | Bin 0 -> 673 bytes sphinx/themes/basic/static/comment.png | Bin 0 -> 3501 bytes sphinx/themes/basic/static/down-pressed.png | Bin 0 -> 368 bytes sphinx/themes/basic/static/down.png | Bin 0 -> 363 bytes sphinx/themes/basic/static/up-pressed.png | Bin 0 -> 372 bytes sphinx/themes/basic/static/up.png | Bin 0 -> 363 bytes 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 sphinx/themes/basic/static/ajax-loader.gif create mode 100644 sphinx/themes/basic/static/comment.png create mode 100644 sphinx/themes/basic/static/down-pressed.png create mode 100644 sphinx/themes/basic/static/down.png create mode 100644 sphinx/themes/basic/static/up-pressed.png create mode 100644 sphinx/themes/basic/static/up.png diff --git a/sphinx/themes/basic/static/ajax-loader.gif b/sphinx/themes/basic/static/ajax-loader.gif new file mode 100644 index 00000000..61faf8ca Binary files /dev/null and b/sphinx/themes/basic/static/ajax-loader.gif differ diff --git a/sphinx/themes/basic/static/comment.png b/sphinx/themes/basic/static/comment.png new file mode 100644 index 00000000..bad742bb Binary files /dev/null and b/sphinx/themes/basic/static/comment.png differ diff --git a/sphinx/themes/basic/static/down-pressed.png b/sphinx/themes/basic/static/down-pressed.png new file mode 100644 index 00000000..6f7ad782 Binary files /dev/null and b/sphinx/themes/basic/static/down-pressed.png differ diff --git a/sphinx/themes/basic/static/down.png b/sphinx/themes/basic/static/down.png new file mode 100644 index 00000000..3003a887 Binary files /dev/null and b/sphinx/themes/basic/static/down.png differ diff --git a/sphinx/themes/basic/static/up-pressed.png b/sphinx/themes/basic/static/up-pressed.png new file mode 100644 index 00000000..8bd587af Binary files /dev/null and b/sphinx/themes/basic/static/up-pressed.png differ diff --git a/sphinx/themes/basic/static/up.png b/sphinx/themes/basic/static/up.png new file mode 100644 index 00000000..b9462568 Binary files /dev/null and b/sphinx/themes/basic/static/up.png differ -- cgit v1.2.1 From 4fadb320c8f7c0098bf1584cfbdd9e955a25b78f Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sun, 8 Aug 2010 15:39:56 -0500 Subject: only provide pygments.css, update image locations. --- sphinx/builders/websupport.py | 14 +++----------- sphinx/themes/basic/static/websupport.js | 12 ++++++------ sphinx/websupport/__init__.py | 9 +++++++++ sphinx/writers/websupport.py | 3 +++ 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index cdd0fb92..3d0356b7 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -81,9 +81,11 @@ class WebSupportBuilder(StandaloneHTMLBuilder): ctx, event_arg) # Create a dict that will be pickled and used by webapps. + css = '<link rel="stylesheet" href="%s" type=text/css />' % \ + pathto('_static/pygmentcs.css', 1) doc_ctx = {'body': ctx.get('body', ''), 'title': ctx.get('title', ''), - 'css': self._make_css(ctx), + 'css': css, 'js': self._make_js(ctx)} # Partially render the html template to proved a more useful ctx. template = self.templates.environment.get_template(templatename) @@ -124,16 +126,6 @@ class WebSupportBuilder(StandaloneHTMLBuilder): def dump_search_index(self): self.indexer.finish_indexing() - def _make_css(self, ctx): - def make_link(file): - path = ctx['pathto'](file, 1) - return '<link rel="stylesheet" href="%s" type=text/css />' % path - - links = [make_link('_static/pygments.css')] - for file in ctx['css_files']: - links.append(make_link(file)) - return '\n'.join(links) - def _make_js(self, ctx): def make_script(file): path = ctx['pathto'](file, 1) diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index 0e2b4430..aeef3ece 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -615,7 +615,7 @@ return this.each(function() { $(this).append( $('<a href="#" class="sphinx_comment"></a>') - .html(opts.commentHTML) + .html('<img src="' + opts.commentImage + '" alt="comment" />') .click(function(event) { event.preventDefault(); show($(this).parent().attr('id')); @@ -722,11 +722,11 @@ acceptCommentURL: '/accept_comment', rejectCommentURL: '/reject_comment', rejectCommentURL: '/delete_comment', - commentHTML: '<img src="/static/comment.png" alt="comment" />', - upArrow: '/static/up.png', - downArrow: '/static/down.png', - upArrowPressed: '/static/up-pressed.png', - downArrowPressed: '/static/down-pressed.png', + commentImage: '/static/_static/comment.png', + upArrow: '/static/_static/up.png', + downArrow: '/static/_static/down.png', + upArrowPressed: '/static/_static/up-pressed.png', + downArrowPressed: '/static/_static/down-pressed.png', voting: false, moderator: false }, COMMENT_OPTIONS); diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 9bd6cbda..3215e856 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -162,6 +162,15 @@ class WebSupport(object): 'reject_comment')) parts.append('deleteCommentURL: "/%s/%s",' % (self.docroot, 'delete_comment')) + + if self.staticdir != 'static': + p = lambda file: '%s/_static/%s' % (self.staticdir, file) + parts.append('commentImage: "/%s",' % p('comment.png') ) + parts.append('upArrow: "/%s",' % p('up.png')) + parts.append('downArrow: "/%s",' % p('down.png')) + parts.append('upArrowPressed: "/%s",' % p('up-pressed.png')) + parts.append('downArrowPressed: "/%s",' % p('down-pressed.png')) + if username is not '': parts.append('voting: true,') parts.append('username: "%s",' % username) diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index 63281f18..688fdbea 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -62,4 +62,7 @@ class WebSupportTranslator(HTMLTranslator): line=node.line, source=node.rawsource, treeloc='???') + if db_node_id == 30711: + import pdb + pdb.set_trace() return db_node_id -- cgit v1.2.1 From 94f975fd5cf5929bcdb039f4c958da9c1739e1ba Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 9 Aug 2010 10:10:11 -0500 Subject: only comment on paragraphs --- sphinx/websupport/storage/sqlalchemystorage.py | 2 +- sphinx/writers/websupport.py | 20 +++++++------------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/sphinx/websupport/storage/sqlalchemystorage.py b/sphinx/websupport/storage/sqlalchemystorage.py index 02fa33b5..2e182311 100644 --- a/sphinx/websupport/storage/sqlalchemystorage.py +++ b/sphinx/websupport/storage/sqlalchemystorage.py @@ -31,7 +31,7 @@ class SQLAlchemyStorage(StorageBackend): node = Node(document, line, source, treeloc) self.build_session.add(node) self.build_session.flush() - return node.id + return node def post_build(self): self.build_session.commit() diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index 688fdbea..64a431d8 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -15,7 +15,7 @@ class WebSupportTranslator(HTMLTranslator): """ Our custom HTML translator. """ - commentable_nodes = ['bullet_list', 'paragraph', 'desc'] + commentable_nodes = ['paragraph'] def __init__(self, builder, *args, **kwargs): HTMLTranslator.__init__(self, builder, *args, **kwargs) @@ -23,8 +23,7 @@ class WebSupportTranslator(HTMLTranslator): self.init_support() def init_support(self): - self.in_commentable = False - self.current_id = 0 + self.cur_node = None def dispatch_visit(self, node): if node.__class__.__name__ in self.commentable_nodes: @@ -39,30 +38,25 @@ class WebSupportTranslator(HTMLTranslator): def handle_visit_commentable(self, node): # If this node is nested inside another commentable node this # node will not be commented. - if not self.in_commentable: - self.in_commentable = True - node_id = self.add_db_node(node) + if self.cur_node is None: + self.cur_node = self.add_db_node(node) # We will place the node in the HTML id attribute. If the node # already has an id (for indexing purposes) put an empty # span with the existing id directly before this node's HTML. if node.attributes['ids']: self.body.append('<span id="%s"></span>' % node.attributes['ids'][0]) - node.attributes['ids'] = ['s%s' % node_id] + node.attributes['ids'] = ['s%s' % self.cur_node.id] node.attributes['classes'].append(self.comment_class) def handle_depart_commentable(self, node): - assert(self.in_commentable) if self.comment_class in node.attributes['classes']: - self.in_commentable = False + self.cur_node = None def add_db_node(self, node): storage = self.builder.app.storage db_node_id = storage.add_node(document=self.builder.cur_docname, line=node.line, - source=node.rawsource, + source=node.rawsource or node.astext(), treeloc='???') - if db_node_id == 30711: - import pdb - pdb.set_trace() return db_node_id -- cgit v1.2.1 From d9852d47ef41b6aec20b1488c9d6e9718c119c6a Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 9 Aug 2010 14:22:31 -0500 Subject: updated docs --- doc/web/api.rst | 43 +++++- doc/web/frontend.rst | 6 - doc/web/quickstart.rst | 140 ++++++++++++----- doc/web/searchadapters.rst | 2 +- doc/web/storagebackends.rst | 26 ++++ doc/websupport.rst | 1 - sphinx/websupport/__init__.py | 205 +++++++++++++++---------- sphinx/websupport/search/__init__.py | 33 ++-- sphinx/websupport/storage/__init__.py | 85 ++++++++-- sphinx/websupport/storage/db.py | 3 +- sphinx/websupport/storage/sqlalchemystorage.py | 8 +- sphinx/writers/websupport.py | 3 +- 12 files changed, 384 insertions(+), 171 deletions(-) delete mode 100644 doc/web/frontend.rst diff --git a/doc/web/api.rst b/doc/web/api.rst index fcd0513e..b2b7ef95 100644 --- a/doc/web/api.rst +++ b/doc/web/api.rst @@ -10,10 +10,45 @@ The WebSupport Class The main API class for the web support package. All interactions with the web support package should occur through this class. - :param srcdir: the directory containing the reStructuredText files - :param outdir: the directory in which to place the built data - :param search: the search system to use - :param comments: an instance of a CommentBackend + The class takes the following keyword arguments: + + srcdir + The directory containing reStructuredText source files. + + builddir + The directory that build data and static files should be placed in. + This should be used when creating a :class:`WebSupport` object that + will be used to build data. + + datadir: + The directory that the web support data is in. This should be used + when creating a :class:`WebSupport` object that will be used to + retrieve data. + + search: + This may contain either a string (e.g. 'xapian') referencing a + built-in search adapter to use, or an instance of a subclass of + :class:`~sphinx.websupport.search.BaseSearch`. + + storage: + This may contain either a string representing a database uri, or an + instance of a subclass of + :class:`~sphinx.websupport.storage.StorageBackend`. If this is not + provided a new sqlite database will be created. + + moderation_callback: + A callable to be called when a new comment is added that is not + displayed. It must accept one argument: a dict representing the + comment that was added. + + staticdir: + If static files are served from a location besides "/static", this + should be a string with the name of that location + (e.g. '/static_files'). + + docroot: + If the documentation is not served from the base path of a URL, this + should be a string specifying that path (e.g. 'docs') Methods ~~~~~~~ diff --git a/doc/web/frontend.rst b/doc/web/frontend.rst deleted file mode 100644 index 5ffe1667..00000000 --- a/doc/web/frontend.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _websupportfrontend: - -Web Support Frontend -==================== - -More coming soon. \ No newline at end of file diff --git a/doc/web/quickstart.rst b/doc/web/quickstart.rst index b0a60507..302a4db0 100644 --- a/doc/web/quickstart.rst +++ b/doc/web/quickstart.rst @@ -7,26 +7,26 @@ Building Documentation Data ~~~~~~~~~~~~~~~~~~~~~~~~~~~ To make use of the web support package in your application you'll -need to build that data it uses. This data includes pickle files representing +need to build the data it uses. This data includes pickle files representing documents, search indices, and node data that is used to track where comments and other things are in a document. To do this you will need -to create an instance of the :class:`~sphinx.websupport.api.WebSupport` +to create an instance of the :class:`~sphinx.websupport.WebSupport` class and call it's :meth:`~sphinx.websupport.WebSupport.build` method:: from sphinx.websupport import WebSupport support = WebSupport(srcdir='/path/to/rst/sources/', - outdir='/path/to/build/outdir', + builddir='/path/to/build/outdir', search='xapian') support.build() This will read reStructuredText sources from `srcdir` and place the -necessary data in `outdir`. This directory contains all the data needed +necessary data in `builddir`. The `builddir` will contain two +sub-directories. One named "data" that contains all the data needed to display documents, search through documents, and add comments to -documents. It will also contain a subdirectory named "static", which -contains static files. These files will be linked to by Sphinx documents, -and should be served from "/static". +documents. The other directory will be called "static" and contains static +files that should be served from "/static". .. note:: @@ -37,7 +37,7 @@ and should be served from "/static". Integrating Sphinx Documents Into Your Webapp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Now that you have the data, it's time to do something useful with it. +Now that the data is built, it's time to do something useful with it. Start off by creating a :class:`~sphinx.websupport.WebSupport` object for your application:: @@ -59,8 +59,8 @@ This will return a dictionary containing the following items: * **sidebar**: The sidebar of the document as HTML * **relbar**: A div containing links to related documents * **title**: The title of the document -* **DOCUMENTATION_OPTIONS**: Javascript containing documentation options -* **COMMENT_OPTIONS**: Javascript containing comment options +* **css**: Links to css files used by Sphinx +* **js**: Javascript containing comment options This dict can then be used as context for templates. The goal is to be easy to integrate with your existing templating system. An example using @@ -74,13 +74,15 @@ easy to integrate with your existing templating system. An example using {{ document.title }} {%- endblock %} + {% block css %} + {{ super() }} + {{ document.css|safe }} + <link rel="stylesheet" href="/static/websupport-custom.css" type="text/css"> + {% endblock %} + {%- block js %} - <script type="text/javascript"> - {{ document.DOCUMENTATION_OPTIONS|safe }} - {{ document.COMMENT_OPTIONS|safe }} - </script> {{ super() }} - <script type="text/javascript" src="/static/websupport.js"></script> + {{ document.js|safe }} {%- endblock %} {%- block relbar %} @@ -99,12 +101,12 @@ Authentication -------------- To use certain features such as voting it must be possible to authenticate -users. The details of the authentication are left to the your application. +users. The details of the authentication are left to your application. Once a user has been authenticated you can pass the user's details to certain :class:`~sphinx.websupport.WebSupport` methods using the *username* and *moderator* keyword arguments. The web support package will store the username with comments and votes. The only caveat is that if you allow users -to change their username, you must update the websupport package's data:: +to change their username you must update the websupport package's data:: support.update_username(old_username, new_username) @@ -113,18 +115,22 @@ should be a boolean representing whether the user has moderation privilieges. The default value for *moderator* is *False*. An example `Flask <http://flask.pocoo.org/>`_ function that checks whether -a user is logged in, and the retrieves a document is:: +a user is logged in and then retrieves a document is:: + + from sphinx.websupport.errors import * @app.route('/<path:docname>') def doc(docname): - if g.user: - document = support.get_document(docname, g.user.name, - g.user.moderator) - else: - document = support.get_document(docname) + username = g.user.name if g.user else '' + moderator = g.user.moderator if g.user else False + try: + document = support.get_document(docname, username, moderator) + except DocumentNotFoundError: + abort(404) return render_template('doc.html', document=document) The first thing to notice is that the *docname* is just the request path. +This makes accessing the correct document easy from a single view. If the user is authenticated then the username and moderation status are passed along with the docname to :meth:`~sphinx.websupport.WebSupport.get_document`. The web support package @@ -134,8 +140,12 @@ will then add this data to the COMMENT_OPTIONS that are used in the template. This only works works if your documentation is served from your document root. If it is served from another directory, you will - need to prefix the url route with that directory:: - + need to prefix the url route with that directory, and give the `docroot` + keyword argument when creating the web support object:: + + support = WebSupport(... + docroot='docs') + @app.route('/docs/<path:docname>') Performing Searches @@ -160,8 +170,8 @@ did to render our documents. That's because dict in the same format that :meth:`~sphinx.websupport.WebSupport.get_document` does. -Comments -~~~~~~~~ +Comments & Proposals +~~~~~~~~~~~~~~~~~~~~ Now that this is done it's time to define the functions that handle the AJAX calls from the script. You will need three functions. The first @@ -171,20 +181,29 @@ function is used to add a new comment, and will call the web support method @app.route('/docs/add_comment', methods=['POST']) def add_comment(): parent_id = request.form.get('parent', '') + node_id = request.form.get('node', '') text = request.form.get('text', '') + proposal = request.form.get('proposal', '') username = g.user.name if g.user is not None else 'Anonymous' - comment = support.add_comment(parent_id, text, username=username) + comment = support.add_comment(text, node_id='node_id', + parent_id='parent_id', + username=username, proposal=proposal) return jsonify(comment=comment) -Then next function handles the retrieval of comments for a specific node, -and is aptly named :meth:`~sphinx.websupport.WebSupport.get_data`:: +You'll notice that both a `parent_id` and `node_id` are sent with the +request. If the comment is being attached directly to a node, `parent_id` +will be empty. If the comment is a child of another comment, then `node_id` +will be empty. Then next function handles the retrieval of comments for a +specific node, and is aptly named +:meth:`~sphinx.websupport.WebSupport.get_data`:: @app.route('/docs/get_comments') def get_comments(): - user_id = g.user.id if g.user else None - parent_id = request.args.get('parent', '') - comments = support.get_data(parent_id, user_id) - return jsonify(comments=comments) + username = g.user.name if g.user else None + moderator = g.user.moderator if g.user else False + node_id = request.args.get('node', '') + data = support.get_data(parent_id, user_id) + return jsonify(**data) The final function that is needed will call :meth:`~sphinx.websupport.WebSupport.process_vote`, and will handle user @@ -201,12 +220,49 @@ votes on comments:: support.process_vote(comment_id, g.user.id, value) return "success" -.. note:: +Comment Moderation +~~~~~~~~~~~~~~~~~~ + +By default all comments added through +:meth:`~sphinx.websupport.WebSupport.add_comment` are automatically +displayed. If you wish to have some form of moderation, you can pass +the `displayed` keyword argument:: + + comment = support.add_comment(text, node_id='node_id', + parent_id='parent_id', + username=username, proposal=proposal, + displayed=False) + +You can then create two new views to handle the moderation of comments. The +first will be called when a moderator decides a comment should be accepted +and displayed:: + + @app.route('/docs/accept_comment', methods=['POST']) + def accept_comment(): + moderator = g.user.moderator if g.user else False + comment_id = request.form.get('id') + support.accept_comment(comment_id, moderator=moderator) + return 'OK' + +The next is very similar, but used when rejecting a comment:: + + @app.route('/docs/reject_comment', methods=['POST']) + def reject_comment(): + moderator = g.user.moderator if g.user else False + comment_id = request.form.get('id') + support.reject_comment(comment_id, moderator=moderator) + return 'OK' + +To perform a custom action (such as emailing a moderator) when a new comment +is added but not displayed, you can pass callable to the +:class:`~sphinx.websupport.WebSupport` class when instantiating your support +object:: + + def moderation_callback(comment): + Do something... + + support = WebSupport(... + moderation_callback=moderation_callback) - Authentication is left up to your existing web application. If you do - not have an existing authentication system there are many readily - available for different frameworks. The web support system stores only - the user's unique integer `user_id` and uses this both for storing votes - and retrieving vote information. It is up to you to ensure that the - user_id passed in is unique, and that the user is authenticated. The - default backend will only allow one vote per comment per `user_id`. +The moderation callback must take one argument, which will be the same +comment dict that is returned by add_comment. \ No newline at end of file diff --git a/doc/web/searchadapters.rst b/doc/web/searchadapters.rst index 83e928ba..e03fee81 100644 --- a/doc/web/searchadapters.rst +++ b/doc/web/searchadapters.rst @@ -11,7 +11,7 @@ and pass that as the `search` keyword argument when you create the :class:`~sphinx.websupport.WebSupport` object:: support = Websupport(srcdir=srcdir, - outdir=outdir, + builddir=builddir, search=MySearch()) For more information about creating a custom search adapter, please see diff --git a/doc/web/storagebackends.rst b/doc/web/storagebackends.rst index 4a10e109..6411bf17 100644 --- a/doc/web/storagebackends.rst +++ b/doc/web/storagebackends.rst @@ -5,6 +5,22 @@ Storage Backends ================ +To create a custom storage backend you will need to subclass the +:class:`~StorageBackend` class. Then create an instance of the new class +and pass that as the `storage` keyword argument when you create the +:class:`~sphinx.websupport.WebSupport` object:: + + support = Websupport(srcdir=srcdir, + builddir=builddir, + storage=MyStorage()) + +For more information about creating a custom storage backend, please see +the documentation of the :class:`StorageBackend` class below. + +.. class:: StorageBackend + + Defines an interface for storage backends. + StorageBackend Methods ~~~~~~~~~~~~~~~~~~~~~~ @@ -16,4 +32,14 @@ StorageBackend Methods .. automethod:: sphinx.websupport.storage.StorageBackend.add_comment +.. automethod:: sphinx.websupport.storage.StorageBackend.delete_comment + .. automethod:: sphinx.websupport.storage.StorageBackend.get_data + +.. automethod:: sphinx.websupport.storage.StorageBackend.process_vote + +.. automethod:: sphinx.websupport.storage.StorageBackend.update_username + +.. automethod:: sphinx.websupport.storage.StorageBackend.accept_comment + +.. automethod:: sphinx.websupport.storage.StorageBackend.reject_comment \ No newline at end of file diff --git a/doc/websupport.rst b/doc/websupport.rst index c7833e7a..59973d74 100644 --- a/doc/websupport.rst +++ b/doc/websupport.rst @@ -11,6 +11,5 @@ into your web application. To learn more read the web/quickstart web/api - web/frontend web/searchadapters web/storagebackends \ No newline at end of file diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 3215e856..38ebd234 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -52,6 +52,8 @@ class WebSupport(object): self._init_search(search) self._init_storage(storage) + self._make_base_comment_options() + def _init_storage(self, storage): if isinstance(storage, StorageBackend): self.storage = storage @@ -90,11 +92,11 @@ class WebSupport(object): """Build the documentation. Places the data into the `outdir` directory. Use it like this:: - support = WebSupport(srcdir, outdir, search='xapian') + support = WebSupport(srcdir, builddir, search='xapian') support.build() - This will read reStructured text files from `srcdir`. Then it - build the pickles and search index, placing them into `outdir`. + This will read reStructured text files from `srcdir`. Then it will + build the pickles and search index, placing them into `builddir`. It will also save node data to the database. """ if not self.srcdir: @@ -116,7 +118,7 @@ class WebSupport(object): be a dict object which can be used to render a template:: support = WebSupport(datadir=datadir) - support.get_document('index') + support.get_document('index', username, moderator) In most cases `docname` will be taken from the request path and passed directly to this function. In Flask, that would be something @@ -124,13 +126,28 @@ class WebSupport(object): @app.route('/<path:docname>') def index(docname): - q = request.args.get('q') - document = support.get_search_results(q) + username = g.user.name if g.user else '' + moderator = g.user.moderator if g.user else False + try: + document = support.get_document(docname, username, + moderator) + except DocumentNotFoundError: + abort(404) render_template('doc.html', document=document) The document dict that is returned contains the following items to be used during template rendering. + * **body**: The main body of the document as HTML + * **sidebar**: The sidebar of the document as HTML + * **relbar**: A div containing links to related documents + * **title**: The title of the document + * **css**: Links to css files used by Sphinx + * **js**: Javascript containing comment options + + This raises :class:`~sphinx.websupport.errors.DocumentNotFoundError` + if a document matching `docname` is not found. + :param docname: the name of the document to load. """ infilename = path.join(self.datadir, 'pickles', docname + '.fpickle') @@ -146,39 +163,6 @@ class WebSupport(object): document['js'] = comment_opts + '\n' + document['js'] return document - def _make_comment_options(self, username, moderator): - parts = ['<script type="text/javascript">', - 'var COMMENT_OPTIONS = {'] - if self.docroot is not '': - parts.append('addCommentURL: "/%s/%s",' % (self.docroot, - 'add_comment')) - parts.append('getCommentsURL: "/%s/%s",' % (self.docroot, - 'get_comments')) - parts.append('processVoteURL: "/%s/%s",' % (self.docroot, - 'process_vote')) - parts.append('acceptCommentURL: "/%s/%s",' % (self.docroot, - 'accept_comment')) - parts.append('rejectCommentURL: "/%s/%s",' % (self.docroot, - 'reject_comment')) - parts.append('deleteCommentURL: "/%s/%s",' % (self.docroot, - 'delete_comment')) - - if self.staticdir != 'static': - p = lambda file: '%s/_static/%s' % (self.staticdir, file) - parts.append('commentImage: "/%s",' % p('comment.png') ) - parts.append('upArrow: "/%s",' % p('up.png')) - parts.append('downArrow: "/%s",' % p('down.png')) - parts.append('upArrowPressed: "/%s",' % p('up-pressed.png')) - parts.append('downArrowPressed: "/%s",' % p('down-pressed.png')) - - if username is not '': - parts.append('voting: true,') - parts.append('username: "%s",' % username) - parts.append('moderator: %s' % str(moderator).lower()) - parts.append('};') - parts.append('</script>') - return '\n'.join(parts) - def get_search_results(self, q): """Perform a search for the query `q`, and create a set of search results. Then render the search results as html and @@ -200,37 +184,42 @@ class WebSupport(object): def get_data(self, node_id, username=None, moderator=False): """Get the comments and source associated with `node_id`. If - `user_id` is given vote information will be included with the - returned comments. The default CommentBackend returns dict with - two keys, *source*, and *comments*. *comments* is a list of - dicts that represent a comment, each having the following items: - - ============ ====================================================== - Key Contents - ============ ====================================================== - text The comment text. - username The username that was stored with the comment. - id The comment's unique identifier. - rating The comment's current rating. - age The time in seconds since the comment was added. - time A dict containing time information. It contains the - following keys: year, month, day, hour, minute, second, - iso, and delta. `iso` is the time formatted in ISO - 8601 format. `delta` is a printable form of how old - the comment is (e.g. "3 hours ago"). - vote If `user_id` was given, this will be an integer - representing the vote. 1 for an upvote, -1 for a - downvote, or 0 if unvoted. - node The node that the comment is attached to. If the - comment's parent is another comment rather than a - node, this will be null. - parent The id of the comment that this comment is attached - to if it is not attached to a node. - children A list of all children, in this format. - ============ ====================================================== + `username` is given vote information will be included with the + returned comments. The default CommentBackend returns a dict with + two keys, *source*, and *comments*. *source* is raw source of the + node and is used as the starting point for proposals a user can + add. *comments* is a list of dicts that represent a comment, each + having the following items: + + ============= ====================================================== + Key Contents + ============= ====================================================== + text The comment text. + username The username that was stored with the comment. + id The comment's unique identifier. + rating The comment's current rating. + age The time in seconds since the comment was added. + time A dict containing time information. It contains the + following keys: year, month, day, hour, minute, second, + iso, and delta. `iso` is the time formatted in ISO + 8601 format. `delta` is a printable form of how old + the comment is (e.g. "3 hours ago"). + vote If `user_id` was given, this will be an integer + representing the vote. 1 for an upvote, -1 for a + downvote, or 0 if unvoted. + node The id of the node that the comment is attached to. + If the comment's parent is another comment rather than + a node, this will be null. + parent The id of the comment that this comment is attached + to if it is not attached to a node. + children A list of all children, in this format. + proposal_diff An HTML representation of the differences between the + the current source and the user's proposed source. + ============= ====================================================== :param node_id: the id of the node to get comments for. - :param user_id: the id of the user viewing the comments. + :param username: the username of the user viewing the comments. + :param moderator: whether the user is a moderator. """ return self.storage.get_data(node_id, username, moderator) @@ -243,6 +232,10 @@ class WebSupport(object): `moderator` is False, the comment will only be deleted if the `username` matches the `username` on the comment. + This raises :class:`~sphinx.websupport.errors.UserNotAuthorizedError` + if moderator is False and `username` doesn't match username on the + comment. + :param comment_id: the id of the comment to delete. :param username: the username requesting the deletion. :param moderator: whether the requestor is a moderator. @@ -250,19 +243,19 @@ class WebSupport(object): self.storage.delete_comment(comment_id, username, moderator) def add_comment(self, text, node_id='', parent_id='', displayed=True, - username=None, rating=0, time=None, proposal=None, + username=None, time=None, proposal=None, moderator=False): """Add a comment to a node or another comment. Returns the comment in the same format as :meth:`get_comments`. If the comment is being attached to a node, pass in the node's id (as a string) with the node keyword argument:: - comment = support.add_comment(text, node=node_id) + comment = support.add_comment(text, node_id=node_id) If the comment is the child of another comment, provide the parent's id (as a string) with the parent keyword argument:: - comment = support.add_comment(text, parent=parent_id) + comment = support.add_comment(text, parent_id=parent_id) If you would like to store a username with the comment, pass in the optional `username` keyword argument:: @@ -274,10 +267,9 @@ class WebSupport(object): :param text: the text of the comment. :param displayed: for moderation purposes :param username: the username of the user making the comment. - :param rating: the starting rating of the comment, defaults to 0. :param time: the time the comment was created, defaults to now. """ - comment = self.storage.add_comment(text, displayed, username, rating, + comment = self.storage.add_comment(text, displayed, username, time, proposal, node_id, parent_id, moderator) if not displayed and self.moderation_callback: @@ -288,10 +280,9 @@ class WebSupport(object): """Process a user's vote. The web support package relies on the API user to perform authentication. The API user will typically receive a comment_id and value from a form, and then - make sure the user is authenticated. A unique integer `user_id` - (usually the User primary key) must be passed in, which will - also be used to retrieve the user's past voting information. - An example, once again in Flask:: + make sure the user is authenticated. A unique username must be + passed in, which will also be used to retrieve the user's past + voting data. An example, once again in Flask:: @app.route('/docs/process_vote', methods=['POST']) def process_vote(): @@ -301,11 +292,11 @@ class WebSupport(object): value = request.form.get('value') if value is None or comment_id is None: abort(400) - support.process_vote(comment_id, g.user.id, value) + support.process_vote(comment_id, g.user.name, value) return "success" :param comment_id: the comment being voted on - :param user_id: the unique integer id of the user voting + :param username: the unique username of the user voting :param value: 1 for an upvote, -1 for a downvote, 0 for an unvote. """ value = int(value) @@ -329,7 +320,11 @@ class WebSupport(object): def accept_comment(self, comment_id, moderator=False): """Accept a comment that is pending moderation. + This raises :class:`~sphinx.websupport.errors.UserNotAuthorizedError` + if moderator is False. + :param comment_id: The id of the comment that was accepted. + :param moderator: Whether the user making the request is a moderator. """ if not moderator: raise UserNotAuthorizedError() @@ -338,8 +333,60 @@ class WebSupport(object): def reject_comment(self, comment_id, moderator=False): """Reject a comment that is pending moderation. + This raises :class:`~sphinx.websupport.errors.UserNotAuthorizedError` + if moderator is False. + :param comment_id: The id of the comment that was accepted. + :param moderator: Whether the user making the request is a moderator. """ if not moderator: raise UserNotAuthorizedError() self.storage.reject_comment(comment_id) + + def _make_base_comment_options(self): + """Helper method to create the part of the COMMENT_OPTIONS javascript + that remains the same throughout the lifetime of the + :class:`~sphinx.websupport.WebSupport` object. + """ + parts = ['<script type="text/javascript">', + 'var COMMENT_OPTIONS = {'] + if self.docroot is not '': + parts.append('addCommentURL: "/%s/%s",' % (self.docroot, + 'add_comment')) + parts.append('getCommentsURL: "/%s/%s",' % (self.docroot, + 'get_comments')) + parts.append('processVoteURL: "/%s/%s",' % (self.docroot, + 'process_vote')) + parts.append('acceptCommentURL: "/%s/%s",' % (self.docroot, + 'accept_comment')) + parts.append('rejectCommentURL: "/%s/%s",' % (self.docroot, + 'reject_comment')) + parts.append('deleteCommentURL: "/%s/%s",' % (self.docroot, + 'delete_comment')) + + if self.staticdir != 'static': + p = lambda file: '%s/_static/%s' % (self.staticdir, file) + parts.append('commentImage: "/%s",' % p('comment.png') ) + parts.append('upArrow: "/%s",' % p('up.png')) + parts.append('downArrow: "/%s",' % p('down.png')) + parts.append('upArrowPressed: "/%s",' % p('up-pressed.png')) + parts.append('downArrowPressed: "/%s",' % p('down-pressed.png')) + + self.base_comment_opts = '\n'.join(parts) + + def _make_comment_options(self, username, moderator): + """Helper method to create the parts of the COMMENT_OPTIONS + javascript that are unique to each request. + + :param username: The username of the user making the request. + :param moderator: Whether the user making the request is a moderator. + """ + parts = [self.base_comment_opts] + if username is not '': + parts.append('voting: true,') + parts.append('username: "%s",' % username) + parts.append('moderator: %s' % str(moderator).lower()) + parts.append('};') + parts.append('</script>') + return '\n'.join(parts) + diff --git a/sphinx/websupport/search/__init__.py b/sphinx/websupport/search/__init__.py index 0e613222..80f91ab1 100644 --- a/sphinx/websupport/search/__init__.py +++ b/sphinx/websupport/search/__init__.py @@ -20,7 +20,7 @@ class BaseSearch(object): is a list of pagenames that will be reindexed. You may want to remove these from the search index before indexing begins. - `param changed` is a list of pagenames that will be re-indexed + :param changed: a list of pagenames that will be re-indexed """ pass @@ -37,11 +37,9 @@ class BaseSearch(object): won't want to override this unless you need access to the `doctree`. Override :meth:`add_document` instead. - `pagename` is the name of the page to be indexed - - `title` is the title of the page to be indexed - - `doctree` is the docutils doctree representation of the page + :param pagename: the name of the page to be indexed + :param title: the title of the page to be indexed + :param doctree: is the docutils doctree representation of the page """ self.add_document(pagename, title, doctree.astext()) @@ -50,18 +48,16 @@ class BaseSearch(object): This method should should do everything necessary to add a single document to the search index. - `pagename` is name of the page being indexed. - It is the combination of the source files relative path and filename, + `pagename` is name of the page being indexed. It is the combination + of the source files relative path and filename, minus the extension. For example, if the source file is "ext/builders.rst", the `pagename` would be "ext/builders". This will need to be returned with search results when processing a query. - - `title` is the page's title, and will need to be returned with - search results. - - `text` is the full text of the page. You probably want to store this - somehow to use while creating the context for search results. + + :param pagename: the name of the page being indexed + :param title: the page's title + :param text: the full text of the page """ raise NotImplementedError() @@ -73,7 +69,7 @@ class BaseSearch(object): don't want to use the included :meth:`extract_context` method. Override :meth:`handle_query` instead. - `q` is the search query string. + :param q: the search query string. """ self.context_re = re.compile('|'.join(q.split()), re.I) return self.handle_query(q) @@ -91,6 +87,8 @@ class BaseSearch(object): The :meth:`extract_context` method is provided as a simple way to create the `context`. + + :param q: the search query """ raise NotImplementedError() @@ -98,9 +96,8 @@ class BaseSearch(object): """Extract the context for the search query from the documents full `text`. - `text` is the full text of the document to create the context for. - - `length` is the length of the context snippet to return. + :param text: the full text of the document to create the context for + :param length: the length of the context snippet to return. """ res = self.context_re.search(text) if res is None: diff --git a/sphinx/websupport/storage/__init__.py b/sphinx/websupport/storage/__init__.py index 6948c8c7..70b23150 100644 --- a/sphinx/websupport/storage/__init__.py +++ b/sphinx/websupport/storage/__init__.py @@ -16,16 +16,14 @@ class StorageBackend(object): """ pass - def add_node(self, document, line, source, treeloc): + def add_node(self, document, line, source): """Add a node to the StorageBackend. - `document` is the name of the document the node belongs to. + :param document: the name of the document the node belongs to. - `line` is the line in the source where the node begins. + :param line: the line in the source where the node begins. - `source` is the source files name. - - `treeloc` is for future use. + :param source: the source files name. """ raise NotImplementedError() @@ -35,14 +33,77 @@ class StorageBackend(object): """ pass - def add_comment(self, text, displayed, username, rating, time, - proposal, node, parent): - """Called when a comment is being added.""" + def add_comment(self, text, displayed, username, time, + proposal, node_id, parent_id, moderator): + """Called when a comment is being added. + + :param text: the text of the comment + :param displayed: whether the comment should be displayed + :param username: the name of the user adding the comment + :param time: a date object with the time the comment was added + :param proposal: the text of the proposal the user made + :param node_id: the id of the node that the comment is being added to + :param parent_id: the id of the comment's parent comment. + :param moderator: whether the user adding the comment is a moderator + """ + raise NotImplementedError() + + def delete_comment(self, comment_id, username, moderator): + """Delete a comment. + + Raises :class:`~sphinx.websupport.errors.UserNotAuthorizedError` + if moderator is False and `username` doesn't match the username + on the comment. + + :param comment_id: The id of the comment being deleted. + :param username: The username of the user requesting the deletion. + :param moderator: Whether the user is a moderator. + """ + raise NotImplementedError() + + def get_data(self, node_id, username, moderator): + """Called to retrieve all data for a node. This should return a + dict with two keys, *source* and *comments* as described by + :class:`~sphinx.websupport.WebSupport`'s + :meth:`~sphinx.websupport.WebSupport.get_data` method. + + :param node_id: The id of the node to get data for. + :param username: The name of the user requesting the data. + :param moderator: Whether the requestor is a moderator. + """ + raise NotImplementedError() + + def process_vote(self, comment_id, username, value): + """Process a vote that is being cast. `value` will be either -1, 0, + or 1. + + :param comment_id: The id of the comment being voted on. + :param username: The username of the user casting the vote. + :param value: The value of the vote being cast. + """ + raise NotImplementedError() + + def update_username(self, old_username, new_username): + """If a user is allowed to change their username this method should + be called so that there is not stagnate data in the storage system. + + :param old_username: The username being changed. + :param new_username: What the username is being changed to. + """ raise NotImplementedError() - def get_data(self, parent_id, user_id, moderator): - """Called to retrieve all comments for a node.""" + def accept_comment(self, comment_id): + """Called when a moderator accepts a comment. After the method is + called the comment should be displayed to all users. + + :param comment_id: The id of the comment being accepted. + """ raise NotImplementedError() - def process_vote(self, comment_id, user_id, value): + def reject_comment(self, comment_id): + """Called when a moderator rejects a comment. The comment should + then be deleted. + + :param comment_id: The id of the comment being accepted. + """ raise NotImplementedError() diff --git a/sphinx/websupport/storage/db.py b/sphinx/websupport/storage/db.py index 12c1e1d5..983eb66d 100644 --- a/sphinx/websupport/storage/db.py +++ b/sphinx/websupport/storage/db.py @@ -81,11 +81,10 @@ class Node(Base): return comments - def __init__(self, document, line, source, treeloc): + def __init__(self, document, line, source): self.document = document self.line = line self.source = source - self.treeloc = treeloc class Comment(Base): __tablename__ = db_prefix + 'comments' diff --git a/sphinx/websupport/storage/sqlalchemystorage.py b/sphinx/websupport/storage/sqlalchemystorage.py index 2e182311..7a906dcb 100644 --- a/sphinx/websupport/storage/sqlalchemystorage.py +++ b/sphinx/websupport/storage/sqlalchemystorage.py @@ -27,8 +27,8 @@ class SQLAlchemyStorage(StorageBackend): def pre_build(self): self.build_session = Session() - def add_node(self, document, line, source, treeloc): - node = Node(document, line, source, treeloc) + def add_node(self, document, line, source): + node = Node(document, line, source) self.build_session.add(node) self.build_session.flush() return node @@ -37,7 +37,7 @@ class SQLAlchemyStorage(StorageBackend): self.build_session.commit() self.build_session.close() - def add_comment(self, text, displayed, username, rating, time, + def add_comment(self, text, displayed, username, time, proposal, node_id, parent_id, moderator): session = Session() @@ -55,7 +55,7 @@ class SQLAlchemyStorage(StorageBackend): else: proposal_diff = None - comment = Comment(text, displayed, username, rating, + comment = Comment(text, displayed, username, 0, time or datetime.now(), proposal, proposal_diff) session.add(comment) session.flush() diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index 64a431d8..05bc2c8b 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -57,6 +57,5 @@ class WebSupportTranslator(HTMLTranslator): storage = self.builder.app.storage db_node_id = storage.add_node(document=self.builder.cur_docname, line=node.line, - source=node.rawsource or node.astext(), - treeloc='???') + source=node.rawsource or node.astext()) return db_node_id -- cgit v1.2.1 From b9e76b2b2ff64c09593a4593c30612b6862d2d38 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 9 Aug 2010 14:36:19 -0500 Subject: updated CHANGES.jacobmason --- CHANGES.jacobmason | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.jacobmason b/CHANGES.jacobmason index bd87c71c..c445006c 100644 --- a/CHANGES.jacobmason +++ b/CHANGES.jacobmason @@ -12,4 +12,16 @@ June 21 - June 26: Implement server side search with two search adapters, one for Xapian and one for Whoosh June 28 - July 12: Implement voting system on the backend, and created a -jQuery script to handle voting on the frontend. \ No newline at end of file +jQuery script to handle voting on the frontend. + +July 13 - July 19: Added documentation for the web support package. + +July 20 - July 27: Added a system to allow user's to propose changes to +documentation along with comments. + +July 28 - August 3: Added tests for the web support package. Refactored +sqlalchemy storage to be more efficient. + +August 4 - August 7: Added comment moderation system. Added more +documentation. General code cleanup. + -- cgit v1.2.1 From 3710823dc4cb582602c3ce662f3aafe48eb16e41 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 9 Aug 2010 14:43:32 -0500 Subject: create a searcher for each query --- sphinx/websupport/search/whooshsearch.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sphinx/websupport/search/whooshsearch.py b/sphinx/websupport/search/whooshsearch.py index 257393a6..0f463531 100644 --- a/sphinx/websupport/search/whooshsearch.py +++ b/sphinx/websupport/search/whooshsearch.py @@ -31,7 +31,6 @@ class WhooshSearch(BaseSearch): self.index = index.open_dir(db_path) else: self.index = index.create_in(db_path, schema=self.schema) - self.searcher = self.index.searcher() def init_indexing(self, changed=[]): for changed_path in changed: @@ -40,8 +39,6 @@ class WhooshSearch(BaseSearch): def finish_indexing(self): self.index_writer.commit() - # Create a new searcher so changes can be seen immediately - self.searcher = self.index.searcher() def add_document(self, pagename, title, text): self.index_writer.add_document(path=unicode(pagename), @@ -49,7 +46,8 @@ class WhooshSearch(BaseSearch): text=text) def handle_query(self, q): - whoosh_results = self.searcher.find('text', q) + searcher = self.index.searcher() + whoosh_results = searcher.find('text', q) results = [] for result in whoosh_results: context = self.extract_context(result['text']) -- cgit v1.2.1 From 6fc396d42c0b7c88662cde02612aac0e3616bfc1 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 9 Aug 2010 14:47:18 -0500 Subject: moved NullSearchException to sphinx.websupport.errors --- sphinx/websupport/errors.py | 7 ++++++- sphinx/websupport/search/nullsearch.py | 7 ++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/sphinx/websupport/errors.py b/sphinx/websupport/errors.py index e78abc21..53106dfb 100644 --- a/sphinx/websupport/errors.py +++ b/sphinx/websupport/errors.py @@ -10,7 +10,8 @@ """ __all__ = ['DocumentNotFoundError', 'SrcdirNotSpecifiedError', - 'UserNotAuthorizedError', 'CommentNotAllowedError'] + 'UserNotAuthorizedError', 'CommentNotAllowedError', + 'NullSearchException'] class DocumentNotFoundError(Exception): pass @@ -26,3 +27,7 @@ class UserNotAuthorizedError(Exception): class CommentNotAllowedError(Exception): pass + + +class NullSearchException(Exception): + pass diff --git a/sphinx/websupport/search/nullsearch.py b/sphinx/websupport/search/nullsearch.py index ad3d7dae..743983c4 100644 --- a/sphinx/websupport/search/nullsearch.py +++ b/sphinx/websupport/search/nullsearch.py @@ -10,11 +10,12 @@ """ from sphinx.websupport.search import BaseSearch - -class NullSearchException(Exception): - pass +from sphinx.websupport.errors import * class NullSearch(BaseSearch): + """A search adapter that does nothing. Used when no search adapter + is specified. + """ def feed(self, pagename, title, doctree): pass -- cgit v1.2.1 From 8279f322a638d50daec691e7aea34487ad2a7ac3 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 9 Aug 2010 15:22:52 -0500 Subject: added more comments/docstrings --- sphinx/websupport/storage/db.py | 31 +++++++++-- sphinx/websupport/storage/differ.py | 73 +++++++++++++++----------- sphinx/websupport/storage/sqlalchemystorage.py | 16 +++--- 3 files changed, 80 insertions(+), 40 deletions(-) diff --git a/sphinx/websupport/storage/db.py b/sphinx/websupport/storage/db.py index 983eb66d..64f7f3e2 100644 --- a/sphinx/websupport/storage/db.py +++ b/sphinx/websupport/storage/db.py @@ -34,6 +34,12 @@ class Node(Base): source = Column(Text, nullable=False) def nested_comments(self, username, moderator): + """Create a tree of comments. First get all comments that are + descendents of this node, then convert them to a tree form. + + :param username: the name of the user to get comments for. + :param moderator: whether the user is moderator. + """ session = Session() if username: @@ -45,24 +51,30 @@ class Node(Base): cvalias = aliased(CommentVote, sq) q = session.query(Comment, cvalias.value).outerjoin(cvalias) else: + # If a username is not provided, we don't need to join with + # CommentVote. q = session.query(Comment) # Filter out all comments not descending from this node. q = q.filter(Comment.path.like(str(self.id) + '.%')) - # Filter out non-displayed comments if this isn't a moderator. + if not moderator: q = q.filter(Comment.displayed == True) + # Retrieve all results. Results must be ordered by Comment.path # so that we can easily transform them from a flat list to a tree. results = q.order_by(Comment.path).all() session.close() - # We now need to convert the flat list of results to a nested - # lists to form the comment tree. Results will by ordered by - # the materialized path. return self._nest_comments(results, username) def _nest_comments(self, results, username): + """Given the flat list of results, convert the list into a + tree. + + :param results: the flat list of comments + :param username: the name of the user requesting the comments. + """ comments = [] list_stack = [comments] for r in results: @@ -87,6 +99,7 @@ class Node(Base): self.source = source class Comment(Base): + """An individual Comment being stored.""" __tablename__ = db_prefix + 'comments' id = Column(Integer, primary_key=True) @@ -110,6 +123,9 @@ class Comment(Base): self.proposal_diff = proposal_diff def set_path(self, node_id, parent_id): + """Set the materialized path for this comment.""" + # This exists because the path can't be set until the session has + # been flushed and this Comment has an id. if node_id: self.path = '%s.%s' % (node_id, self.id) else: @@ -120,6 +136,9 @@ class Comment(Base): self.path = '%s.%s' % (parent_path, self.id) def serializable(self, vote=0): + """Creates a serializable representation of the comment. This is + converted to JSON, and used on the client side. + """ delta = datetime.now() - self.time time = {'year': self.time.year, @@ -149,6 +168,9 @@ class Comment(Base): 'children': []} def pretty_delta(self, delta): + """Create a pretty representation of the Comment's age. + (e.g. 2 minutes). + """ days = delta.days seconds = delta.seconds hours = seconds / 3600 @@ -162,6 +184,7 @@ class Comment(Base): return '%s %s ago' % dt if dt[0] == 1 else '%s %ss ago' % dt class CommentVote(Base): + """A vote a user has made on a Comment.""" __tablename__ = db_prefix + 'commentvote' username = Column(String(64), primary_key=True) diff --git a/sphinx/websupport/storage/differ.py b/sphinx/websupport/storage/differ.py index c82ba742..068d7e6f 100644 --- a/sphinx/websupport/storage/differ.py +++ b/sphinx/websupport/storage/differ.py @@ -14,39 +14,18 @@ from cgi import escape from difflib import Differ class CombinedHtmlDiff(object): - + """Create an HTML representation of the differences between two pieces + of text. + """ highlight_regex = re.compile(r'([\+\-\^]+)') - def _highlight_text(self, text, next, tag): - next = next[2:] - new_text = [] - start = 0 - for match in self.highlight_regex.finditer(next): - new_text.append(text[start:match.start()]) - new_text.append('<%s>' % tag) - new_text.append(text[match.start():match.end()]) - new_text.append('</%s>' % tag) - start = match.end() - new_text.append(text[start:]) - return ''.join(new_text) - - def _handle_line(self, line, next=None): - prefix = line[0] - text = line[2:] - - if prefix == ' ': - return text - elif prefix == '?': - return '' - - if next is not None and next[0] == '?': - tag = 'ins' if prefix == '+' else 'del' - text = self._highlight_text(text, next, tag) - css_class = 'prop_added' if prefix == '+' else 'prop_removed' - - return '<span class="%s">%s</span>\n' % (css_class, text.rstrip()) - def make_html(self, source, proposal): + """Return the HTML representation of the differences between + `source` and `proposal`. + + :param source: the original text + :param proposal: the proposed text + """ proposal = escape(proposal) differ = Differ() @@ -64,3 +43,37 @@ class CombinedHtmlDiff(object): self._handle_line(line) break return ''.join(html) + + def _handle_line(self, line, next=None): + """Handle an individual line in a diff.""" + prefix = line[0] + text = line[2:] + + if prefix == ' ': + return text + elif prefix == '?': + return '' + + if next is not None and next[0] == '?': + tag = 'ins' if prefix == '+' else 'del' + text = self._highlight_text(text, next, tag) + css_class = 'prop_added' if prefix == '+' else 'prop_removed' + + return '<span class="%s">%s</span>\n' % (css_class, text.rstrip()) + + def _highlight_text(self, text, next, tag): + """Highlight the specific changes made to a line by adding + <ins> and <del> tags. + """ + next = next[2:] + new_text = [] + start = 0 + for match in self.highlight_regex.finditer(next): + new_text.append(text[start:match.start()]) + new_text.append('<%s>' % tag) + new_text.append(text[match.start():match.end()]) + new_text.append('</%s>' % tag) + start = match.end() + new_text.append(text[start:]) + return ''.join(new_text) + diff --git a/sphinx/websupport/storage/sqlalchemystorage.py b/sphinx/websupport/storage/sqlalchemystorage.py index 7a906dcb..553450d3 100644 --- a/sphinx/websupport/storage/sqlalchemystorage.py +++ b/sphinx/websupport/storage/sqlalchemystorage.py @@ -18,6 +18,9 @@ from sphinx.websupport.storage.db import Base, Node, Comment, CommentVote,\ from sphinx.websupport.storage.differ import CombinedHtmlDiff class SQLAlchemyStorage(StorageBackend): + """A :class:`~sphinx.websupport.storage.StorageBackend` using + SQLAlchemy. + """ def __init__(self, engine): self.engine = engine Base.metadata.bind = engine @@ -40,7 +43,8 @@ class SQLAlchemyStorage(StorageBackend): def add_comment(self, text, displayed, username, time, proposal, node_id, parent_id, moderator): session = Session() - + proposal_diff = None + if node_id and proposal: node = session.query(Node).filter(Node.id == node_id).one() differ = CombinedHtmlDiff() @@ -51,19 +55,18 @@ class SQLAlchemyStorage(StorageBackend): if not parent.displayed: raise CommentNotAllowedError( "Can't add child to a parent that is not displayed") - proposal_diff = None - else: - proposal_diff = None comment = Comment(text, displayed, username, 0, time or datetime.now(), proposal, proposal_diff) session.add(comment) session.flush() + # We have to flush the session before setting the path so the + # Comment has an id. comment.set_path(node_id, parent_id) session.commit() - comment = comment.serializable() + d = comment.serializable() session.close() - return comment + return d def delete_comment(self, comment_id, username, moderator): session = Session() @@ -72,6 +75,7 @@ class SQLAlchemyStorage(StorageBackend): if moderator or comment.username == username: comment.username = '[deleted]' comment.text = '[deleted]' + comment.proposal = '' session.commit() session.close() else: -- cgit v1.2.1 From 9e89208c9e83f5f118bf4b1f716256d1cfd4c8f9 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 9 Aug 2010 15:46:54 -0500 Subject: A few changes to sqlalchemystorage --- sphinx/websupport/storage/sqlalchemystorage.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/sphinx/websupport/storage/sqlalchemystorage.py b/sphinx/websupport/storage/sqlalchemystorage.py index 553450d3..94318a96 100644 --- a/sphinx/websupport/storage/sqlalchemystorage.py +++ b/sphinx/websupport/storage/sqlalchemystorage.py @@ -11,6 +11,8 @@ from datetime import datetime +from sqlalchemy.orm import aliased + from sphinx.websupport.errors import * from sphinx.websupport.storage import StorageBackend from sphinx.websupport.storage.db import Base, Node, Comment, CommentVote,\ @@ -92,12 +94,13 @@ class SQLAlchemyStorage(StorageBackend): def process_vote(self, comment_id, username, value): session = Session() - vote = session.query(CommentVote).filter( - CommentVote.comment_id == comment_id).filter( - CommentVote.username == username).first() - comment = session.query(Comment).filter( - Comment.id == comment_id).first() + subquery = session.query(CommentVote).filter( + CommentVote.username == username).subquery() + vote_alias = aliased(CommentVote, subquery) + q = session.query(Comment, vote_alias).outerjoin(vote_alias).filter( + Comment.id == comment_id) + comment, vote = q.one() if vote is None: vote = CommentVote(comment_id, username, value) @@ -105,32 +108,39 @@ class SQLAlchemyStorage(StorageBackend): else: comment.rating += value - vote.value vote.value = value + session.add(vote) session.commit() session.close() def update_username(self, old_username, new_username): session = Session() + session.query(Comment).filter(Comment.username == old_username).\ update({Comment.username: new_username}) session.query(CommentVote).\ filter(CommentVote.username == old_username).\ update({CommentVote.username: new_username}) + session.commit() session.close() def accept_comment(self, comment_id): session = Session() - comment = session.query(Comment).\ - filter(Comment.id == comment_id).one() - comment.displayed = True + + comment = session.query(Comment).filter( + Comment.id == comment_id).update( + {Comment.displayed: True}) + session.commit() session.close() def reject_comment(self, comment_id): session = Session() + comment = session.query(Comment).\ filter(Comment.id == comment_id).one() session.delete(comment) + session.commit() session.close() -- cgit v1.2.1 From 882350a6340473e3ca04175288f9eea9ed5749df Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 9 Aug 2010 16:47:29 -0500 Subject: hide proposal textarea after a comment is added --- sphinx/themes/basic/static/websupport.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index aeef3ece..62912d6b 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -212,18 +212,22 @@ function addComment(form) { // Disable the form that is being submitted. form.find('textarea,input').attr('disabled', 'disabled'); + var node_id = form.find('input[name="node"]').val(); // Send the comment to the server. $.ajax({ type: "POST", url: opts.addCommentURL, dataType: 'json', - data: {node: form.find('input[name="node"]').val(), + data: {node: node_id, parent: form.find('input[name="parent"]').val(), text: form.find('textarea[name="comment"]').val(), proposal: form.find('textarea[name="proposal"]').val()}, success: function(data, textStatus, error) { // Reset the form. + if (node_id) { + hideProposeChange(node_id); + } form.find('textarea') .val('') .add(form.find('input')) @@ -378,7 +382,7 @@ $('a.hide_propose_change').hide(); $('a.show_propose_change').show(); var textarea = $('textarea[name="proposal"]'); - textarea.val(''); + textarea.val('').removeAttr('disabled'); textarea.slideUp('fast'); }; -- cgit v1.2.1 From b325f99dc7f85c43fd69cedddf1650a2273c13af Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 9 Aug 2010 16:53:29 -0500 Subject: fixed comment reply width --- sphinx/themes/basic/static/websupport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index 62912d6b..c4fa5755 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -630,7 +630,7 @@ var replyTemplate = ' <li>\ <div class="reply_div" id="rd<%id%>">\ <form id="rf<%id%>">\ - <textarea name="comment"></textarea>\ + <textarea name="comment" cols="80"></textarea>\ <input type="submit" value="add reply" />\ <input type="hidden" name="parent" value="<%id%>" />\ <input type="hidden" name="node" value="" />\ -- cgit v1.2.1 From 043bba7b529cf83880a18d58cc2519fe24373617 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 9 Aug 2010 18:31:13 -0500 Subject: added get_metadata --- sphinx/websupport/__init__.py | 15 ++++++++++++++- sphinx/websupport/storage/__init__.py | 9 +++++++++ sphinx/websupport/storage/db.py | 5 +++++ sphinx/websupport/storage/sqlalchemystorage.py | 14 ++++++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 38ebd234..090fee1a 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -160,7 +160,11 @@ class WebSupport(object): document = pickle.load(f) comment_opts = self._make_comment_options(username, moderator) - document['js'] = comment_opts + '\n' + document['js'] + comment_metadata = self.storage.get_metadata(docname, moderator) + + document['js'] = '\n'.join([comment_opts, + self._make_metadata(comment_metadata), + document['js']]) return document def get_search_results(self, q): @@ -390,3 +394,12 @@ class WebSupport(object): parts.append('</script>') return '\n'.join(parts) + def _make_metadata(self, data): + node_js = ', '.join(['%s: %s' % (node_id, comment_count) + for node_id, comment_count in data.iteritems()]) + js = """ +<script type="text/javascript"> + var COMMENT_METADATA = {%s}; +</script>""" % node_js + return js + diff --git a/sphinx/websupport/storage/__init__.py b/sphinx/websupport/storage/__init__.py index 70b23150..24d4ade5 100644 --- a/sphinx/websupport/storage/__init__.py +++ b/sphinx/websupport/storage/__init__.py @@ -61,6 +61,15 @@ class StorageBackend(object): """ raise NotImplementedError() + def get_metadata(self, docname, moderator): + """Get metadata for a document. This is currently just a dict + of node_id's with associated comment counts. + + :param docname: the name of the document to get metadata for. + :param moderator: whether the requester is a moderator. + """ + raise NotImplementedError() + def get_data(self, node_id, username, moderator): """Called to retrieve all data for a node. This should return a dict with two keys, *source* and *comments* as described by diff --git a/sphinx/websupport/storage/db.py b/sphinx/websupport/storage/db.py index 64f7f3e2..4a84cd08 100644 --- a/sphinx/websupport/storage/db.py +++ b/sphinx/websupport/storage/db.py @@ -112,6 +112,9 @@ class Comment(Base): proposal_diff = Column(Text) path = Column(String(256), index=True) + node_id = Column(Integer, ForeignKey(db_prefix + 'nodes.id')) + node = relation(Node, backref="comments") + def __init__(self, text, displayed, username, rating, time, proposal, proposal_diff): self.text = text @@ -127,12 +130,14 @@ class Comment(Base): # This exists because the path can't be set until the session has # been flushed and this Comment has an id. if node_id: + self.node_id = node_id self.path = '%s.%s' % (node_id, self.id) else: session = Session() parent_path = session.query(Comment.path).\ filter(Comment.id == parent_id).one().path session.close() + self.node_id = parent_path.split('.')[0] self.path = '%s.%s' % (parent_path, self.id) def serializable(self, vote=0): diff --git a/sphinx/websupport/storage/sqlalchemystorage.py b/sphinx/websupport/storage/sqlalchemystorage.py index 94318a96..1aaa8473 100644 --- a/sphinx/websupport/storage/sqlalchemystorage.py +++ b/sphinx/websupport/storage/sqlalchemystorage.py @@ -12,6 +12,7 @@ from datetime import datetime from sqlalchemy.orm import aliased +from sqlalchemy.sql import func from sphinx.websupport.errors import * from sphinx.websupport.storage import StorageBackend @@ -84,6 +85,19 @@ class SQLAlchemyStorage(StorageBackend): session.close() raise UserNotAuthorizedError() + def get_metadata(self, docname, moderator): + session = Session() + subquery = session.query( + Comment.id, Comment.node_id, + func.count('*').label('comment_count')).group_by( + Comment.node_id).subquery() + nodes = session.query(Node.id, subquery.c.comment_count).outerjoin( + (subquery, Node.id==subquery.c.node_id)).filter( + Node.document==docname) + session.close() + session.commit() + return dict([(k, v or 0) for k, v in nodes]) + def get_data(self, node_id, username, moderator): session = Session() node = session.query(Node).filter(Node.id == node_id).one() -- cgit v1.2.1 From ea4668abe4a075997d5c3dada219f851dc9e9505 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 9 Aug 2010 19:07:18 -0500 Subject: added comment metadata to frontend --- sphinx/themes/basic/static/comment-bright.png | Bin 0 -> 3500 bytes sphinx/themes/basic/static/websupport.js | 8 +++++++- sphinx/websupport/__init__.py | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 sphinx/themes/basic/static/comment-bright.png diff --git a/sphinx/themes/basic/static/comment-bright.png b/sphinx/themes/basic/static/comment-bright.png new file mode 100644 index 00000000..551517b8 Binary files /dev/null and b/sphinx/themes/basic/static/comment-bright.png differ diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index c4fa5755..99d1a221 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -617,9 +617,14 @@ */ $.fn.comment = function() { return this.each(function() { + var id = $(this).attr('id').substring(1); + var count = COMMENT_METADATA[id] + var title = count + ' comment' + (count == 1 ? '' : 's'); + var image = count > 0 ? opts.commentBrightImage : opts.commentImage; $(this).append( $('<a href="#" class="sphinx_comment"></a>') - .html('<img src="' + opts.commentImage + '" alt="comment" />') + .html('<img src="' + image + '" alt="comment" />') + .attr('title', title) .click(function(event) { event.preventDefault(); show($(this).parent().attr('id')); @@ -727,6 +732,7 @@ rejectCommentURL: '/reject_comment', rejectCommentURL: '/delete_comment', commentImage: '/static/_static/comment.png', + commentBrightImage: '/static/_static/comment-bright.png', upArrow: '/static/_static/up.png', downArrow: '/static/_static/down.png', upArrowPressed: '/static/_static/up-pressed.png', diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 090fee1a..76715d14 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -371,6 +371,8 @@ class WebSupport(object): if self.staticdir != 'static': p = lambda file: '%s/_static/%s' % (self.staticdir, file) parts.append('commentImage: "/%s",' % p('comment.png') ) + parts.append( + 'commentBrightImage: "/%s",' % p('comment-bright.png') ) parts.append('upArrow: "/%s",' % p('up.png')) parts.append('downArrow: "/%s",' % p('down.png')) parts.append('upArrowPressed: "/%s",' % p('up-pressed.png')) -- cgit v1.2.1 From 5b3789fc4506d4bb05365ef8195cd1bbf2f1cae3 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Tue, 10 Aug 2010 12:25:48 +0200 Subject: Initial version tracking implementation --- sphinx/util/__init__.py | 36 +++++++++ sphinx/versioning.py | 145 ++++++++++++++++++++++++++++++++++ tests/root/contents.txt | 1 + tests/root/versioning/added.txt | 20 +++++ tests/root/versioning/deleted.txt | 12 +++ tests/root/versioning/deleted_end.txt | 11 +++ tests/root/versioning/index.txt | 11 +++ tests/root/versioning/insert.txt | 18 +++++ tests/root/versioning/modified.txt | 17 ++++ tests/root/versioning/original.txt | 15 ++++ tests/test_versioning.py | 88 +++++++++++++++++++++ 11 files changed, 374 insertions(+) create mode 100644 sphinx/versioning.py create mode 100644 tests/root/versioning/added.txt create mode 100644 tests/root/versioning/deleted.txt create mode 100644 tests/root/versioning/deleted_end.txt create mode 100644 tests/root/versioning/index.txt create mode 100644 tests/root/versioning/insert.txt create mode 100644 tests/root/versioning/modified.txt create mode 100644 tests/root/versioning/original.txt create mode 100644 tests/test_versioning.py diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index ec48009f..a434f3a8 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 00000000..51fd28a9 --- /dev/null +++ b/sphinx/versioning.py @@ -0,0 +1,145 @@ +# -*- 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, old_node in enumerate(old_nodes): + if merge_node(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] + new_nodes = [n for n in new_nodes if not hasattr(n, 'uid')] + 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 differs from the `old` one with at least + one character. + + 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 e052e04b..280953b4 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 00000000..22a70739 --- /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 00000000..a1a9c4c9 --- /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 00000000..f30e6300 --- /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 00000000..234e223f --- /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 00000000..1c157cc9 --- /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 00000000..49cdad93 --- /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 00000000..b3fe0609 --- /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_versioning.py b/tests/test_versioning.py new file mode 100644 index 00000000..19ef8904 --- /dev/null +++ b/tests/test_versioning.py @@ -0,0 +1,88 @@ +# -*- 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() + +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(): + from nose import SkipTest + raise SkipTest('The algorithm does not work at the moment') + 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:] -- cgit v1.2.1 From 27e0abd589be8a12f9889feb78e3102d62d97af3 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Tue, 10 Aug 2010 12:31:10 +0200 Subject: Fixed documentation --- sphinx/versioning.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sphinx/versioning.py b/sphinx/versioning.py index 51fd28a9..1a9c1ced 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -109,8 +109,7 @@ def make_diff(old, new): changed - ``True`` if the `new` string differs from the `old` one with at least - one character. + ``True`` if the `new` string is a changed version of the `old` one. replaced -- cgit v1.2.1 From c03634e844d7d507bb5fc3796099b70680212daf Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Tue, 10 Aug 2010 13:15:03 +0200 Subject: Fix line length --- sphinx/versioning.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sphinx/versioning.py b/sphinx/versioning.py index 1a9c1ced..9ba7e3f7 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -87,7 +87,8 @@ def merge_doctrees(old, new, condition): 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)): + 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] -- cgit v1.2.1 From 9ae387d9fdcb4bdaf1dd44217d0d8fdd9f965ede Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Tue, 10 Aug 2010 15:22:11 +0200 Subject: Fixed algorithm test_insert passes now and everything seems to be working fine --- sphinx/versioning.py | 4 ++-- tests/test_versioning.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/sphinx/versioning.py b/sphinx/versioning.py index 9ba7e3f7..5806e971 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -67,8 +67,8 @@ def merge_doctrees(old, new, condition): continue if not merge_node(old_node, new_node): if old_nodes: - for i, old_node in enumerate(old_nodes): - if merge_node(old_node, new_node): + 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 diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 19ef8904..47c322bb 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -78,8 +78,6 @@ def test_deleted_end(): assert original_uids[:-1] == uids def test_insert(): - from nose import SkipTest - raise SkipTest('The algorithm does not work at the moment') insert = doctrees['versioning/insert'] new_nodes = list(merge_doctrees(original, insert, is_paragraph)) uids = [n.uid for n in insert.traverse(is_paragraph)] -- cgit v1.2.1 From 7dbd85c6aca4e84d3123b94733d8526ac58d61a4 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Tue, 10 Aug 2010 15:22:42 +0200 Subject: Delete remaining files in the _build directory --- tests/test_versioning.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 47c322bb..77306580 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -25,6 +25,7 @@ def setup_module(): def teardown_module(): app.cleanup() + (test_root / '_build').rmtree(True) doctrees = {} -- cgit v1.2.1 From e76149081932acf783f8c2b336aa28d45c2c7a8c Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Tue, 10 Aug 2010 15:29:57 +0200 Subject: Removed unnecessary check --- sphinx/versioning.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sphinx/versioning.py b/sphinx/versioning.py index 5806e971..42b8bd51 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -92,7 +92,6 @@ def merge_doctrees(old, new, condition): if merge_node(old_node, new_node): del new_nodes[i] del old_nodes[j] - new_nodes = [n for n in new_nodes if not hasattr(n, 'uid')] for node in new_nodes: node.uid = uuid4().hex # Yielding the new nodes here makes it possible to use this generator -- cgit v1.2.1 From 8573faabb10140e1500cb6582ff7092805fc5bb1 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Tue, 10 Aug 2010 17:11:11 +0200 Subject: Removed trailing whitespace --- doc/web/api.rst | 87 ++++++++++----------- doc/web/quickstart.rst | 104 ++++++++++++------------- doc/web/searchadapters.rst | 21 +++-- doc/web/storagebackends.rst | 10 +-- doc/websupport.rst | 10 +-- sphinx/builders/websupport.py | 5 +- sphinx/websupport/__init__.py | 73 +++++++++-------- sphinx/websupport/search/__init__.py | 13 ++-- sphinx/websupport/search/xapiansearch.py | 2 +- sphinx/websupport/storage/__init__.py | 6 +- sphinx/websupport/storage/db.py | 6 +- sphinx/websupport/storage/differ.py | 5 +- sphinx/websupport/storage/sqlalchemystorage.py | 6 +- tests/test_searchadapters.py | 3 +- tests/test_websupport.py | 28 +++---- 15 files changed, 186 insertions(+), 193 deletions(-) diff --git a/doc/web/api.rst b/doc/web/api.rst index b2b7ef95..b63e6864 100644 --- a/doc/web/api.rst +++ b/doc/web/api.rst @@ -7,49 +7,49 @@ The WebSupport Class .. class:: WebSupport - The main API class for the web support package. All interactions - with the web support package should occur through this class. - - The class takes the following keyword arguments: - - srcdir - The directory containing reStructuredText source files. - - builddir - The directory that build data and static files should be placed in. - This should be used when creating a :class:`WebSupport` object that - will be used to build data. - - datadir: - The directory that the web support data is in. This should be used - when creating a :class:`WebSupport` object that will be used to - retrieve data. - - search: - This may contain either a string (e.g. 'xapian') referencing a - built-in search adapter to use, or an instance of a subclass of - :class:`~sphinx.websupport.search.BaseSearch`. - - storage: - This may contain either a string representing a database uri, or an - instance of a subclass of - :class:`~sphinx.websupport.storage.StorageBackend`. If this is not - provided a new sqlite database will be created. - - moderation_callback: - A callable to be called when a new comment is added that is not - displayed. It must accept one argument: a dict representing the - comment that was added. - - staticdir: - If static files are served from a location besides "/static", this - should be a string with the name of that location - (e.g. '/static_files'). - - docroot: - If the documentation is not served from the base path of a URL, this - should be a string specifying that path (e.g. 'docs') - + The main API class for the web support package. All interactions + with the web support package should occur through this class. + + The class takes the following keyword arguments: + + srcdir + The directory containing reStructuredText source files. + + builddir + The directory that build data and static files should be placed in. + This should be used when creating a :class:`WebSupport` object that + will be used to build data. + + datadir: + The directory that the web support data is in. This should be used + when creating a :class:`WebSupport` object that will be used to + retrieve data. + + search: + This may contain either a string (e.g. 'xapian') referencing a + built-in search adapter to use, or an instance of a subclass of + :class:`~sphinx.websupport.search.BaseSearch`. + + storage: + This may contain either a string representing a database uri, or an + instance of a subclass of + :class:`~sphinx.websupport.storage.StorageBackend`. If this is not + provided a new sqlite database will be created. + + moderation_callback: + A callable to be called when a new comment is added that is not + displayed. It must accept one argument: a dict representing the + comment that was added. + + staticdir: + If static files are served from a location besides "/static", this + should be a string with the name of that location + (e.g. '/static_files'). + + docroot: + If the documentation is not served from the base path of a URL, this + should be a string specifying that path (e.g. 'docs') + Methods ~~~~~~~ @@ -64,4 +64,3 @@ Methods .. automethod:: sphinx.websupport.WebSupport.process_vote .. automethod:: sphinx.websupport.WebSupport.get_search_results - diff --git a/doc/web/quickstart.rst b/doc/web/quickstart.rst index 302a4db0..de9b7655 100644 --- a/doc/web/quickstart.rst +++ b/doc/web/quickstart.rst @@ -10,22 +10,22 @@ To make use of the web support package in your application you'll need to build the data it uses. This data includes pickle files representing documents, search indices, and node data that is used to track where comments and other things are in a document. To do this you will need -to create an instance of the :class:`~sphinx.websupport.WebSupport` +to create an instance of the :class:`~sphinx.websupport.WebSupport` class and call it's :meth:`~sphinx.websupport.WebSupport.build` method:: from sphinx.websupport import WebSupport support = WebSupport(srcdir='/path/to/rst/sources/', builddir='/path/to/build/outdir', - search='xapian') + search='xapian') support.build() This will read reStructuredText sources from `srcdir` and place the -necessary data in `builddir`. The `builddir` will contain two +necessary data in `builddir`. The `builddir` will contain two sub-directories. One named "data" that contains all the data needed to display documents, search through documents, and add comments to -documents. The other directory will be called "static" and contains static +documents. The other directory will be called "static" and contains static files that should be served from "/static". .. note:: @@ -40,14 +40,14 @@ Integrating Sphinx Documents Into Your Webapp Now that the data is built, it's time to do something useful with it. Start off by creating a :class:`~sphinx.websupport.WebSupport` object for your application:: - + from sphinx.websupport import WebSupport support = WebSupport(datadir='/path/to/the/data', search='xapian') You'll only need one of these for each set of documentation you will be -working with. You can then call it's +working with. You can then call it's :meth:`~sphinx.websupport.WebSupport.get_document` method to access individual documents:: @@ -56,14 +56,14 @@ individual documents:: This will return a dictionary containing the following items: * **body**: The main body of the document as HTML -* **sidebar**: The sidebar of the document as HTML +* **sidebar**: The sidebar of the document as HTML * **relbar**: A div containing links to related documents * **title**: The title of the document * **css**: Links to css files used by Sphinx * **js**: Javascript containing comment options This dict can then be used as context for templates. The goal is to be -easy to integrate with your existing templating system. An example using +easy to integrate with your existing templating system. An example using `Jinja2 <http://jinja.pocoo.org/2/>`_ is: .. sourcecode:: html+jinja @@ -71,30 +71,30 @@ easy to integrate with your existing templating system. An example using {%- extends "layout.html" %} {%- block title %} - {{ document.title }} + {{ document.title }} {%- endblock %} {% block css %} - {{ super() }} - {{ document.css|safe }} - <link rel="stylesheet" href="/static/websupport-custom.css" type="text/css"> + {{ super() }} + {{ document.css|safe }} + <link rel="stylesheet" href="/static/websupport-custom.css" type="text/css"> {% endblock %} {%- block js %} - {{ super() }} - {{ document.js|safe }} + {{ super() }} + {{ document.js|safe }} {%- endblock %} {%- block relbar %} - {{ document.relbar|safe }} + {{ document.relbar|safe }} {%- endblock %} {%- block body %} - {{ document.body|safe }} + {{ document.body|safe }} {%- endblock %} {%- block sidebar %} - {{ document.sidebar|safe }} + {{ document.sidebar|safe }} {%- endblock %} Authentication @@ -108,7 +108,7 @@ Once a user has been authenticated you can pass the user's details to certain username with comments and votes. The only caveat is that if you allow users to change their username you must update the websupport package's data:: - support.update_username(old_username, new_username) + support.update_username(old_username, new_username) *username* should be a unique string which identifies a user, and *moderator* should be a boolean representing whether the user has moderation @@ -121,32 +121,32 @@ a user is logged in and then retrieves a document is:: @app.route('/<path:docname>') def doc(docname): - username = g.user.name if g.user else '' - moderator = g.user.moderator if g.user else False - try: - document = support.get_document(docname, username, moderator) - except DocumentNotFoundError: - abort(404) + username = g.user.name if g.user else '' + moderator = g.user.moderator if g.user else False + try: + document = support.get_document(docname, username, moderator) + except DocumentNotFoundError: + abort(404) return render_template('doc.html', document=document) The first thing to notice is that the *docname* is just the request path. This makes accessing the correct document easy from a single view. If the user is authenticated then the username and moderation status are -passed along with the docname to +passed along with the docname to :meth:`~sphinx.websupport.WebSupport.get_document`. The web support package will then add this data to the COMMENT_OPTIONS that are used in the template. .. note:: - This only works works if your documentation is served from your - document root. If it is served from another directory, you will - need to prefix the url route with that directory, and give the `docroot` - keyword argument when creating the web support object:: - - support = WebSupport(... - docroot='docs') - - @app.route('/docs/<path:docname>') + This only works works if your documentation is served from your + document root. If it is served from another directory, you will + need to prefix the url route with that directory, and give the `docroot` + keyword argument when creating the web support object:: + + support = WebSupport(... + docroot='docs') + + @app.route('/docs/<path:docname>') Performing Searches ~~~~~~~~~~~~~~~~~~~ @@ -155,7 +155,7 @@ To use the search form built-in to the Sphinx sidebar, create a function to handle requests to the url 'search' relative to the documentation root. The user's search query will be in the GET parameters, with the key `q`. Then use the :meth:`~sphinx.websupport.WebSupport.get_search_results` method -to retrieve search results. In `Flask <http://flask.pocoo.org/>`_ that +to retrieve search results. In `Flask <http://flask.pocoo.org/>`_ that would be like this:: @app.route('/search') @@ -165,7 +165,7 @@ would be like this:: return render_template('doc.html', document=document) Note that we used the same template to render our search results as we -did to render our documents. That's because +did to render our documents. That's because :meth:`~sphinx.websupport.WebSupport.get_search_results` returns a context dict in the same format that :meth:`~sphinx.websupport.WebSupport.get_document` does. @@ -186,22 +186,22 @@ function is used to add a new comment, and will call the web support method proposal = request.form.get('proposal', '') username = g.user.name if g.user is not None else 'Anonymous' comment = support.add_comment(text, node_id='node_id', - parent_id='parent_id', - username=username, proposal=proposal) + parent_id='parent_id', + username=username, proposal=proposal) return jsonify(comment=comment) You'll notice that both a `parent_id` and `node_id` are sent with the request. If the comment is being attached directly to a node, `parent_id` will be empty. If the comment is a child of another comment, then `node_id` -will be empty. Then next function handles the retrieval of comments for a -specific node, and is aptly named +will be empty. Then next function handles the retrieval of comments for a +specific node, and is aptly named :meth:`~sphinx.websupport.WebSupport.get_data`:: @app.route('/docs/get_comments') def get_comments(): username = g.user.name if g.user else None - moderator = g.user.moderator if g.user else False - node_id = request.args.get('node', '') + moderator = g.user.moderator if g.user else False + node_id = request.args.get('node', '') data = support.get_data(parent_id, user_id) return jsonify(**data) @@ -223,15 +223,15 @@ votes on comments:: Comment Moderation ~~~~~~~~~~~~~~~~~~ -By default all comments added through +By default all comments added through :meth:`~sphinx.websupport.WebSupport.add_comment` are automatically displayed. If you wish to have some form of moderation, you can pass the `displayed` keyword argument:: comment = support.add_comment(text, node_id='node_id', - parent_id='parent_id', + parent_id='parent_id', username=username, proposal=proposal, - displayed=False) + displayed=False) You can then create two new views to handle the moderation of comments. The first will be called when a moderator decides a comment should be accepted @@ -240,18 +240,18 @@ and displayed:: @app.route('/docs/accept_comment', methods=['POST']) def accept_comment(): moderator = g.user.moderator if g.user else False - comment_id = request.form.get('id') - support.accept_comment(comment_id, moderator=moderator) - return 'OK' + comment_id = request.form.get('id') + support.accept_comment(comment_id, moderator=moderator) + return 'OK' The next is very similar, but used when rejecting a comment:: @app.route('/docs/reject_comment', methods=['POST']) def reject_comment(): moderator = g.user.moderator if g.user else False - comment_id = request.form.get('id') - support.reject_comment(comment_id, moderator=moderator) - return 'OK' + comment_id = request.form.get('id') + support.reject_comment(comment_id, moderator=moderator) + return 'OK' To perform a custom action (such as emailing a moderator) when a new comment is added but not displayed, you can pass callable to the @@ -265,4 +265,4 @@ object:: moderation_callback=moderation_callback) The moderation callback must take one argument, which will be the same -comment dict that is returned by add_comment. \ No newline at end of file +comment dict that is returned by add_comment. diff --git a/doc/web/searchadapters.rst b/doc/web/searchadapters.rst index e03fee81..a84aa8da 100644 --- a/doc/web/searchadapters.rst +++ b/doc/web/searchadapters.rst @@ -10,26 +10,26 @@ To create a custom search adapter you will need to subclass the and pass that as the `search` keyword argument when you create the :class:`~sphinx.websupport.WebSupport` object:: - support = Websupport(srcdir=srcdir, - builddir=builddir, - search=MySearch()) + support = Websupport(srcdir=srcdir, + builddir=builddir, + search=MySearch()) For more information about creating a custom search adapter, please see the documentation of the :class:`BaseSearch` class below. .. class:: BaseSearch - Defines an interface for search adapters. + Defines an interface for search adapters. BaseSearch Methods ~~~~~~~~~~~~~~~~~~ - The following methods are defined in the BaseSearch class. Some methods - do not need to be overridden, but some ( - :meth:`~sphinx.websupport.search.BaseSearch.add_document` and - :meth:`~sphinx.websupport.search.BaseSearch.handle_query`) must be - overridden in your subclass. For a working example, look at the - built-in adapter for whoosh. + The following methods are defined in the BaseSearch class. Some methods + do not need to be overridden, but some ( + :meth:`~sphinx.websupport.search.BaseSearch.add_document` and + :meth:`~sphinx.websupport.search.BaseSearch.handle_query`) must be + overridden in your subclass. For a working example, look at the + built-in adapter for whoosh. .. automethod:: sphinx.websupport.search.BaseSearch.init_indexing @@ -44,4 +44,3 @@ BaseSearch Methods .. automethod:: sphinx.websupport.search.BaseSearch.handle_query .. automethod:: sphinx.websupport.search.BaseSearch.extract_context - diff --git a/doc/web/storagebackends.rst b/doc/web/storagebackends.rst index 6411bf17..6b701ea3 100644 --- a/doc/web/storagebackends.rst +++ b/doc/web/storagebackends.rst @@ -10,16 +10,16 @@ To create a custom storage backend you will need to subclass the and pass that as the `storage` keyword argument when you create the :class:`~sphinx.websupport.WebSupport` object:: - support = Websupport(srcdir=srcdir, - builddir=builddir, - storage=MyStorage()) + support = Websupport(srcdir=srcdir, + builddir=builddir, + storage=MyStorage()) For more information about creating a custom storage backend, please see the documentation of the :class:`StorageBackend` class below. .. class:: StorageBackend - Defines an interface for storage backends. + Defines an interface for storage backends. StorageBackend Methods ~~~~~~~~~~~~~~~~~~~~~~ @@ -42,4 +42,4 @@ StorageBackend Methods .. automethod:: sphinx.websupport.storage.StorageBackend.accept_comment -.. automethod:: sphinx.websupport.storage.StorageBackend.reject_comment \ No newline at end of file +.. automethod:: sphinx.websupport.storage.StorageBackend.reject_comment diff --git a/doc/websupport.rst b/doc/websupport.rst index 59973d74..4d743719 100644 --- a/doc/websupport.rst +++ b/doc/websupport.rst @@ -4,12 +4,12 @@ Sphinx Web Support ================== Sphinx provides a way to easily integrate Sphinx documentation -into your web application. To learn more read the +into your web application. To learn more read the :ref:`websupportquickstart`. .. toctree:: - web/quickstart - web/api - web/searchadapters - web/storagebackends \ No newline at end of file + web/quickstart + web/api + web/searchadapters + web/storagebackends diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 3d0356b7..23d0f52c 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -58,7 +58,7 @@ class WebSupportBuilder(StandaloneHTMLBuilder): def handle_page(self, pagename, addctx, templatename='page.html', outfilename=None, event_arg=None): # This is mostly copied from StandaloneHTMLBuilder. However, instead - # of rendering the template and saving the html, create a context + # of rendering the template and saving the html, create a context # dict and pickle it. ctx = self.globalcontext.copy() ctx['pagename'] = pagename @@ -140,7 +140,7 @@ class WebSupportBuilder(StandaloneHTMLBuilder): FILE_SUFFIX: '', HAS_SOURCE: '%s' }; -</script>""" +</script>""" opts = opts % (ctx.get('url_root', ''), escape(ctx['release']), str(ctx['has_source']).lower()) scripts = [] @@ -148,4 +148,3 @@ class WebSupportBuilder(StandaloneHTMLBuilder): scripts.append(make_script(file)) scripts.append(make_script('_static/websupport.js')) return opts + '\n' + '\n'.join(scripts) - diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 76715d14..17aef402 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -49,7 +49,7 @@ class WebSupport(object): self.moderation_callback = moderation_callback self._init_templating() - self._init_search(search) + self._init_search(search) self._init_storage(storage) self._make_base_comment_options() @@ -105,7 +105,7 @@ class WebSupport(object): doctreedir = path.join(self.outdir, 'doctrees') app = WebSupportApp(self.srcdir, self.srcdir, self.outdir, doctreedir, 'websupport', - search=self.search, status=self.status, + search=self.search, status=self.status, warning=self.warning, storage=self.storage, staticdir=self.staticdir, builddir=self.builddir) @@ -119,8 +119,8 @@ class WebSupport(object): support = WebSupport(datadir=datadir) support.get_document('index', username, moderator) - - In most cases `docname` will be taken from the request path and + + In most cases `docname` will be taken from the request path and passed directly to this function. In Flask, that would be something like this:: @@ -139,7 +139,7 @@ class WebSupport(object): to be used during template rendering. * **body**: The main body of the document as HTML - * **sidebar**: The sidebar of the document as HTML + * **sidebar**: The sidebar of the document as HTML * **relbar**: A div containing links to related documents * **title**: The title of the document * **css**: Links to css files used by Sphinx @@ -161,7 +161,7 @@ class WebSupport(object): document = pickle.load(f) comment_opts = self._make_comment_options(username, moderator) comment_metadata = self.storage.get_metadata(docname, moderator) - + document['js'] = '\n'.join([comment_opts, self._make_metadata(comment_metadata), document['js']]) @@ -172,7 +172,7 @@ class WebSupport(object): of search results. Then render the search results as html and return a context dict like the one created by :meth:`get_document`:: - + document = support.get_search_results(q) :param q: the search query @@ -187,12 +187,12 @@ class WebSupport(object): return document def get_data(self, node_id, username=None, moderator=False): - """Get the comments and source associated with `node_id`. If - `username` is given vote information will be included with the + """Get the comments and source associated with `node_id`. If + `username` is given vote information will be included with the returned comments. The default CommentBackend returns a dict with two keys, *source*, and *comments*. *source* is raw source of the node and is used as the starting point for proposals a user can - add. *comments* is a list of dicts that represent a comment, each + add. *comments* is a list of dicts that represent a comment, each having the following items: ============= ====================================================== @@ -209,12 +209,12 @@ class WebSupport(object): 8601 format. `delta` is a printable form of how old the comment is (e.g. "3 hours ago"). vote If `user_id` was given, this will be an integer - representing the vote. 1 for an upvote, -1 for a + representing the vote. 1 for an upvote, -1 for a downvote, or 0 if unvoted. - node The id of the node that the comment is attached to. - If the comment's parent is another comment rather than + node The id of the node that the comment is attached to. + If the comment's parent is another comment rather than a node, this will be null. - parent The id of the comment that this comment is attached + parent The id of the comment that this comment is attached to if it is not attached to a node. children A list of all children, in this format. proposal_diff An HTML representation of the differences between the @@ -232,7 +232,7 @@ class WebSupport(object): instead replaces the username and text files with "[deleted]" so as not to leave any comments orphaned. - If `moderator` is True, the comment will always be deleted. If + If `moderator` is True, the comment will always be deleted. If `moderator` is False, the comment will only be deleted if the `username` matches the `username` on the comment. @@ -246,25 +246,25 @@ class WebSupport(object): """ self.storage.delete_comment(comment_id, username, moderator) - def add_comment(self, text, node_id='', parent_id='', displayed=True, + def add_comment(self, text, node_id='', parent_id='', displayed=True, username=None, time=None, proposal=None, moderator=False): - """Add a comment to a node or another comment. Returns the comment - in the same format as :meth:`get_comments`. If the comment is being - attached to a node, pass in the node's id (as a string) with the + """Add a comment to a node or another comment. Returns the comment + in the same format as :meth:`get_comments`. If the comment is being + attached to a node, pass in the node's id (as a string) with the node keyword argument:: comment = support.add_comment(text, node_id=node_id) If the comment is the child of another comment, provide the parent's id (as a string) with the parent keyword argument:: - + comment = support.add_comment(text, parent_id=parent_id) If you would like to store a username with the comment, pass in the optional `username` keyword argument:: - comment = support.add_comment(text, node=node_id, + comment = support.add_comment(text, node=node_id, username=username) :param parent_id: the prefixed id of the comment's parent. @@ -274,7 +274,7 @@ class WebSupport(object): :param time: the time the comment was created, defaults to now. """ comment = self.storage.add_comment(text, displayed, username, - time, proposal, node_id, + time, proposal, node_id, parent_id, moderator) if not displayed and self.moderation_callback: self.moderation_callback(comment) @@ -282,10 +282,10 @@ class WebSupport(object): def process_vote(self, comment_id, username, value): """Process a user's vote. The web support package relies - on the API user to perform authentication. The API user will + on the API user to perform authentication. The API user will typically receive a comment_id and value from a form, and then - make sure the user is authenticated. A unique username must be - passed in, which will also be used to retrieve the user's past + make sure the user is authenticated. A unique username must be + passed in, which will also be used to retrieve the user's past voting data. An example, once again in Flask:: @app.route('/docs/process_vote', methods=['POST']) @@ -352,20 +352,20 @@ class WebSupport(object): that remains the same throughout the lifetime of the :class:`~sphinx.websupport.WebSupport` object. """ - parts = ['<script type="text/javascript">', + parts = ['<script type="text/javascript">', 'var COMMENT_OPTIONS = {'] if self.docroot is not '': - parts.append('addCommentURL: "/%s/%s",' % (self.docroot, + parts.append('addCommentURL: "/%s/%s",' % (self.docroot, 'add_comment')) - parts.append('getCommentsURL: "/%s/%s",' % (self.docroot, + parts.append('getCommentsURL: "/%s/%s",' % (self.docroot, 'get_comments')) - parts.append('processVoteURL: "/%s/%s",' % (self.docroot, + parts.append('processVoteURL: "/%s/%s",' % (self.docroot, 'process_vote')) - parts.append('acceptCommentURL: "/%s/%s",' % (self.docroot, + parts.append('acceptCommentURL: "/%s/%s",' % (self.docroot, 'accept_comment')) - parts.append('rejectCommentURL: "/%s/%s",' % (self.docroot, + parts.append('rejectCommentURL: "/%s/%s",' % (self.docroot, 'reject_comment')) - parts.append('deleteCommentURL: "/%s/%s",' % (self.docroot, + parts.append('deleteCommentURL: "/%s/%s",' % (self.docroot, 'delete_comment')) if self.staticdir != 'static': @@ -378,8 +378,8 @@ class WebSupport(object): parts.append('upArrowPressed: "/%s",' % p('up-pressed.png')) parts.append('downArrowPressed: "/%s",' % p('down-pressed.png')) - self.base_comment_opts = '\n'.join(parts) - + self.base_comment_opts = '\n'.join(parts) + def _make_comment_options(self, username, moderator): """Helper method to create the parts of the COMMENT_OPTIONS javascript that are unique to each request. @@ -394,14 +394,13 @@ class WebSupport(object): parts.append('moderator: %s' % str(moderator).lower()) parts.append('};') parts.append('</script>') - return '\n'.join(parts) + return '\n'.join(parts) def _make_metadata(self, data): - node_js = ', '.join(['%s: %s' % (node_id, comment_count) + node_js = ', '.join(['%s: %s' % (node_id, comment_count) for node_id, comment_count in data.iteritems()]) js = """ <script type="text/javascript"> var COMMENT_METADATA = {%s}; </script>""" % node_js return js - diff --git a/sphinx/websupport/search/__init__.py b/sphinx/websupport/search/__init__.py index 80f91ab1..cb66618b 100644 --- a/sphinx/websupport/search/__init__.py +++ b/sphinx/websupport/search/__init__.py @@ -34,7 +34,7 @@ class BaseSearch(object): def feed(self, pagename, title, doctree): """Called by the builder to add a doctree to the index. Converts the `doctree` to text and passes it to :meth:`add_document`. You probably - won't want to override this unless you need access to the `doctree`. + won't want to override this unless you need access to the `doctree`. Override :meth:`add_document` instead. :param pagename: the name of the page to be indexed @@ -50,11 +50,11 @@ class BaseSearch(object): `pagename` is name of the page being indexed. It is the combination of the source files relative path and filename, - minus the extension. For example, if the source file is + minus the extension. For example, if the source file is "ext/builders.rst", the `pagename` would be "ext/builders". This - will need to be returned with search results when processing a + will need to be returned with search results when processing a query. - + :param pagename: the name of the page being indexed :param title: the page's title :param text: the full text of the page @@ -62,13 +62,13 @@ class BaseSearch(object): raise NotImplementedError() def query(self, q): - """Called by the web support api to get search results. This method + """Called by the web support api to get search results. This method compiles the regular expression to be used when :meth:`extracting context <extract_context>`, then calls :meth:`handle_query`. You won't want to override this unless you don't want to use the included :meth:`extract_context` method. Override :meth:`handle_query` instead. - + :param q: the search query string. """ self.context_re = re.compile('|'.join(q.split()), re.I) @@ -119,4 +119,3 @@ search_adapters = { 'whoosh': ('whooshsearch', 'WhooshSearch'), 'null': ('nullsearch', 'NullSearch') } - diff --git a/sphinx/websupport/search/xapiansearch.py b/sphinx/websupport/search/xapiansearch.py index 2f2ffbe5..16c7e2b1 100644 --- a/sphinx/websupport/search/xapiansearch.py +++ b/sphinx/websupport/search/xapiansearch.py @@ -28,7 +28,7 @@ class XapianSearch(BaseSearch): def init_indexing(self, changed=[]): ensuredir(self.db_path) - self.database = xapian.WritableDatabase(self.db_path, + self.database = xapian.WritableDatabase(self.db_path, xapian.DB_CREATE_OR_OPEN) self.indexer = xapian.TermGenerator() stemmer = xapian.Stem("english") diff --git a/sphinx/websupport/storage/__init__.py b/sphinx/websupport/storage/__init__.py index 24d4ade5..17907e99 100644 --- a/sphinx/websupport/storage/__init__.py +++ b/sphinx/websupport/storage/__init__.py @@ -20,13 +20,13 @@ class StorageBackend(object): """Add a node to the StorageBackend. :param document: the name of the document the node belongs to. - + :param line: the line in the source where the node begins. :param source: the source files name. """ raise NotImplementedError() - + def post_build(self): """Called after a build has completed. Use this to finalize the addition of nodes if needed. @@ -36,7 +36,7 @@ class StorageBackend(object): def add_comment(self, text, displayed, username, time, proposal, node_id, parent_id, moderator): """Called when a comment is being added. - + :param text: the text of the comment :param displayed: whether the comment should be displayed :param username: the name of the user adding the comment diff --git a/sphinx/websupport/storage/db.py b/sphinx/websupport/storage/db.py index 4a84cd08..74a3e2b7 100644 --- a/sphinx/websupport/storage/db.py +++ b/sphinx/websupport/storage/db.py @@ -81,7 +81,7 @@ class Node(Base): comment, vote = r if username else (r, 0) inheritance_chain = comment.path.split('.')[1:] - + if len(inheritance_chain) == len(list_stack) + 1: parent = list_stack[-1][-1] list_stack.append(parent['children']) @@ -90,7 +90,7 @@ class Node(Base): list_stack.pop() list_stack[-1].append(comment.serializable(vote=vote)) - + return comments def __init__(self, document, line, source): @@ -115,7 +115,7 @@ class Comment(Base): node_id = Column(Integer, ForeignKey(db_prefix + 'nodes.id')) node = relation(Node, backref="comments") - def __init__(self, text, displayed, username, rating, time, + def __init__(self, text, displayed, username, rating, time, proposal, proposal_diff): self.text = text self.displayed = displayed diff --git a/sphinx/websupport/storage/differ.py b/sphinx/websupport/storage/differ.py index 068d7e6f..f0b6a8ea 100644 --- a/sphinx/websupport/storage/differ.py +++ b/sphinx/websupport/storage/differ.py @@ -53,12 +53,12 @@ class CombinedHtmlDiff(object): return text elif prefix == '?': return '' - + if next is not None and next[0] == '?': tag = 'ins' if prefix == '+' else 'del' text = self._highlight_text(text, next, tag) css_class = 'prop_added' if prefix == '+' else 'prop_removed' - + return '<span class="%s">%s</span>\n' % (css_class, text.rstrip()) def _highlight_text(self, text, next, tag): @@ -76,4 +76,3 @@ class CombinedHtmlDiff(object): start = match.end() new_text.append(text[start:]) return ''.join(new_text) - diff --git a/sphinx/websupport/storage/sqlalchemystorage.py b/sphinx/websupport/storage/sqlalchemystorage.py index 1aaa8473..e2cd87ac 100644 --- a/sphinx/websupport/storage/sqlalchemystorage.py +++ b/sphinx/websupport/storage/sqlalchemystorage.py @@ -21,7 +21,7 @@ from sphinx.websupport.storage.db import Base, Node, Comment, CommentVote,\ from sphinx.websupport.storage.differ import CombinedHtmlDiff class SQLAlchemyStorage(StorageBackend): - """A :class:`~sphinx.websupport.storage.StorageBackend` using + """A :class:`~sphinx.websupport.storage.StorageBackend` using SQLAlchemy. """ def __init__(self, engine): @@ -59,7 +59,7 @@ class SQLAlchemyStorage(StorageBackend): raise CommentNotAllowedError( "Can't add child to a parent that is not displayed") - comment = Comment(text, displayed, username, 0, + comment = Comment(text, displayed, username, 0, time or datetime.now(), proposal, proposal_diff) session.add(comment) session.flush() @@ -88,7 +88,7 @@ class SQLAlchemyStorage(StorageBackend): def get_metadata(self, docname, moderator): session = Session() subquery = session.query( - Comment.id, Comment.node_id, + Comment.id, Comment.node_id, func.count('*').label('comment_count')).group_by( Comment.node_id).subquery() nodes = session.query(Node.id, subquery.c.comment_count).outerjoin( diff --git a/tests/test_searchadapters.py b/tests/test_searchadapters.py index cb6c6e96..a30141df 100644 --- a/tests/test_searchadapters.py +++ b/tests/test_searchadapters.py @@ -27,7 +27,7 @@ def teardown_module(): def search_adapter_helper(adapter): clear_builddir() - + settings = {'builddir': os.path.join(test_root, 'websupport'), 'status': StringIO(), 'warning': StringIO()} @@ -81,4 +81,3 @@ def test_whoosh(): except ImportError: sys.stderr.write('info: not running whoosh tests, ' \ 'whoosh doesn\'t seem to be installed') - diff --git a/tests/test_websupport.py b/tests/test_websupport.py index 27a14e36..32249976 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -64,7 +64,7 @@ def test_build(support): @with_support() def test_get_document(support): raises(DocumentNotFoundError, support.get_document, 'nonexisting') - + contents = support.get_document('contents') assert contents['title'] and contents['body'] \ and contents['sidebar'] and contents['relbar'] @@ -78,27 +78,27 @@ def test_comments(support): second_node = nodes[1] # Create a displayed comment and a non displayed comment. - comment = support.add_comment('First test comment', + comment = support.add_comment('First test comment', node_id=str(first_node.id), username='user_one') - hidden_comment = support.add_comment('Hidden comment', - node_id=str(first_node.id), + hidden_comment = support.add_comment('Hidden comment', + node_id=str(first_node.id), displayed=False) # Make sure that comments can't be added to a comment where # displayed == False, since it could break the algorithm that # converts a nodes comments to a tree. - raises(CommentNotAllowedError, support.add_comment, 'Not allowed', + raises(CommentNotAllowedError, support.add_comment, 'Not allowed', parent_id=str(hidden_comment['id'])) # Add a displayed and not displayed child to the displayed comment. support.add_comment('Child test comment', parent_id=str(comment['id']), username='user_one') - support.add_comment('Hidden child test comment', + support.add_comment('Hidden child test comment', parent_id=str(comment['id']), displayed=False) # Add a comment to another node to make sure it isn't returned later. - support.add_comment('Second test comment', + support.add_comment('Second test comment', node_id=str(second_node.id), username='user_two') - + # Access the comments as a moderator. data = support.get_data(str(first_node.id), moderator=True) comments = data['comments'] @@ -130,7 +130,7 @@ def test_voting(support): data = support.get_data(str(node.id)) comment = data['comments'][0] assert comment['rating'] == val, '%s != %s' % (comment['rating'], val) - + support.process_vote(comment['id'], 'user_one', '1') support.process_vote(comment['id'], 'user_two', '1') support.process_vote(comment['id'], 'user_three', '1') @@ -161,7 +161,7 @@ def test_proposals(support): source = data['source'] proposal = source[:5] + source[10:15] + 'asdf' + source[15:] - comment = support.add_comment('Proposal comment', + comment = support.add_comment('Proposal comment', node_id=str(node.id), proposal=proposal) @@ -195,7 +195,7 @@ def test_moderator_delete_comments(support): return support.get_data(str(node.id), moderator=True)['comments'][1] comment = get_comment() - support.delete_comment(comment['id'], username='user_two', + support.delete_comment(comment['id'], username='user_two', moderator=True) comment = get_comment() assert comment['username'] == '[deleted]' @@ -228,9 +228,9 @@ def moderation_callback(comment): @with_support(moderation_callback=moderation_callback) def test_moderation(support): - accepted = support.add_comment('Accepted Comment', node_id=3, + accepted = support.add_comment('Accepted Comment', node_id=3, displayed=False) - rejected = support.add_comment('Rejected comment', node_id=3, + rejected = support.add_comment('Rejected comment', node_id=3, displayed=False) # Make sure the moderation_callback is called. assert called == True @@ -248,7 +248,7 @@ def test_moderation(support): def test_differ(): differ = CombinedHtmlDiff() source = 'Lorem ipsum dolor sit amet,\nconsectetur adipisicing elit,\n' \ - 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' + 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' prop = 'Lorem dolor sit amet,\nconsectetur nihil adipisicing elit,\n' \ 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' differ.make_html(source, prop) -- cgit v1.2.1 From 15360eb1bdd2102231027fef95627a36247bd507 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Tue, 10 Aug 2010 17:16:49 +0200 Subject: Fix test_config under 2.x. --- tests/test_config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_config.py b/tests/test_config.py index 7fce4495..b5f88a6f 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' -- cgit v1.2.1 From d5beb92db99f0fa1616b8ba67f549d225807cfa6 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Tue, 10 Aug 2010 12:49:12 -0500 Subject: fixed typo --- sphinx/builders/websupport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 3d0356b7..e789ead5 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -82,7 +82,7 @@ class WebSupportBuilder(StandaloneHTMLBuilder): # Create a dict that will be pickled and used by webapps. css = '<link rel="stylesheet" href="%s" type=text/css />' % \ - pathto('_static/pygmentcs.css', 1) + pathto('_static/pygment.css', 1) doc_ctx = {'body': ctx.get('body', ''), 'title': ctx.get('title', ''), 'css': css, -- cgit v1.2.1 From 4e40e55c77d2d7714e57a617b331b92b58139ae4 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Tue, 10 Aug 2010 13:04:23 -0500 Subject: fixed ajax-loader.gif path --- sphinx/themes/basic/static/websupport.js | 4 +++- sphinx/websupport/__init__.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index 99d1a221..11d812fd 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -718,7 +718,8 @@ <a href="#" class="sort_option" id="age">oldest</a>\ </p>\ </form>\ - <h3 id="comment_notification">loading comments... <img src="/static/ajax-loader.gif" alt="" /></h3>\ + <h3 id="comment_notification">loading comments... <img src="' + + opts.loadingImage + '" alt="" /></h3>\ <ul id="comment_ul"></ul>\ </div>\ <div id="focuser"></div>'; @@ -732,6 +733,7 @@ rejectCommentURL: '/reject_comment', rejectCommentURL: '/delete_comment', commentImage: '/static/_static/comment.png', + loadingImage: '/static/_static/comment.png', commentBrightImage: '/static/_static/comment-bright.png', upArrow: '/static/_static/up.png', downArrow: '/static/_static/down.png', diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 76715d14..2ac195de 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -371,6 +371,7 @@ class WebSupport(object): if self.staticdir != 'static': p = lambda file: '%s/_static/%s' % (self.staticdir, file) parts.append('commentImage: "/%s",' % p('comment.png') ) + parts.append('loadingImage: "/%s",' % p('ajax-loader.gif') ) parts.append( 'commentBrightImage: "/%s",' % p('comment-bright.png') ) parts.append('upArrow: "/%s",' % p('up.png')) -- cgit v1.2.1 From 374402205488b2acefcc8c0ea8a2025a2fecc013 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Tue, 10 Aug 2010 20:09:07 +0200 Subject: Don't create Javascript by ourselves, we have sphinx.util.jsonimpl.dumps for that --- sphinx/websupport/__init__.py | 82 +++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 17aef402..1b33dfa3 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -11,6 +11,7 @@ import sys import cPickle as pickle +import posixpath from os import path from datetime import datetime @@ -18,6 +19,7 @@ from jinja2 import Environment, FileSystemLoader from sphinx.application import Sphinx from sphinx.util.osutil import ensuredir +from sphinx.util.jsonimpl import dumps as dump_json from sphinx.websupport.search import BaseSearch, search_adapters from sphinx.websupport.storage import StorageBackend from sphinx.websupport.errors import * @@ -352,33 +354,28 @@ class WebSupport(object): that remains the same throughout the lifetime of the :class:`~sphinx.websupport.WebSupport` object. """ - parts = ['<script type="text/javascript">', - 'var COMMENT_OPTIONS = {'] - if self.docroot is not '': - parts.append('addCommentURL: "/%s/%s",' % (self.docroot, - 'add_comment')) - parts.append('getCommentsURL: "/%s/%s",' % (self.docroot, - 'get_comments')) - parts.append('processVoteURL: "/%s/%s",' % (self.docroot, - 'process_vote')) - parts.append('acceptCommentURL: "/%s/%s",' % (self.docroot, - 'accept_comment')) - parts.append('rejectCommentURL: "/%s/%s",' % (self.docroot, - 'reject_comment')) - parts.append('deleteCommentURL: "/%s/%s",' % (self.docroot, - 'delete_comment')) - - if self.staticdir != 'static': - p = lambda file: '%s/_static/%s' % (self.staticdir, file) - parts.append('commentImage: "/%s",' % p('comment.png') ) - parts.append( - 'commentBrightImage: "/%s",' % p('comment-bright.png') ) - parts.append('upArrow: "/%s",' % p('up.png')) - parts.append('downArrow: "/%s",' % p('down.png')) - parts.append('upArrowPressed: "/%s",' % p('up-pressed.png')) - parts.append('downArrowPressed: "/%s",' % p('down-pressed.png')) - - self.base_comment_opts = '\n'.join(parts) + comment_urls = [ + ('addCommentURL', 'add_comment'), + ('getCommentsURL', 'get_comments'), + ('processVoteURL', 'process_vote'), + ('acceptCommentURL', 'accept_comment'), + ('rejectCommentURL', 'reject_comment'), + ('deleteCommentURL', 'delete_comment') + ] + static_urls = [ + ('commentImage', 'comment.png'), + ('commentBrightImage', 'comment-bright.png'), + ('upArrow', 'up.png'), + ('upArrowPressed', 'up-pressed.png'), + ('downArrow', 'down.png'), + ('downArrowPressed', 'down-pressed.png') + ] + + self.base_comment_opts = {} + for key, value in comment_urls: + self.base_comment_opts[key] = posixpath.join(self.docroot, value) + for key, value in static_urls: + self.base_comment_opts[key] = posixpath.join(self.staticdir, value) def _make_comment_options(self, username, moderator): """Helper method to create the parts of the COMMENT_OPTIONS @@ -388,19 +385,22 @@ class WebSupport(object): :param moderator: Whether the user making the request is a moderator. """ parts = [self.base_comment_opts] - if username is not '': - parts.append('voting: true,') - parts.append('username: "%s",' % username) - parts.append('moderator: %s' % str(moderator).lower()) - parts.append('};') - parts.append('</script>') - return '\n'.join(parts) + rv = self.base_comment_opts.copy() + if username: + rv.update({ + 'voting': True, + 'username': username, + 'moderator': str(moderator).lower(), + }) + return '\n'.join([ + '<script type="text/javascript">', + 'var COMMENT_OPTIONS = %s;' % dump_json(rv), + '</script>' + ]) def _make_metadata(self, data): - node_js = ', '.join(['%s: %s' % (node_id, comment_count) - for node_id, comment_count in data.iteritems()]) - js = """ -<script type="text/javascript"> - var COMMENT_METADATA = {%s}; -</script>""" % node_js - return js + return '\n'.join([ + '<script type="text/javascript">', + 'var COMMENT_METADATA = %s;' % dump_json(data), + '</script>' + ]) -- cgit v1.2.1 From 3d5028d24de75661d2c4e406c0b55e70dfef0f0c Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Tue, 10 Aug 2010 14:40:11 -0500 Subject: That typo I fixed... still not right... *slaps self in face* --- sphinx/builders/websupport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index c1decde4..30cf2831 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -82,7 +82,7 @@ class WebSupportBuilder(StandaloneHTMLBuilder): # Create a dict that will be pickled and used by webapps. css = '<link rel="stylesheet" href="%s" type=text/css />' % \ - pathto('_static/pygment.css', 1) + pathto('_static/pygments.css', 1) doc_ctx = {'body': ctx.get('body', ''), 'title': ctx.get('title', ''), 'css': css, -- cgit v1.2.1 From 8dc2fb308c28174aeb04c7084804a898cfdd70b1 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Tue, 10 Aug 2010 21:50:48 +0200 Subject: Switch to sphinx.util.jsonimpl.dumps in the builder --- sphinx/builders/websupport.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index c1decde4..a801eada 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) -- cgit v1.2.1 From 59b6787e92645a11801c60d14e6f1a7a135494ec Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Tue, 10 Aug 2010 14:59:22 -0500 Subject: move opts in js --- sphinx/themes/basic/static/websupport.js | 37 ++++++++++++++++---------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index 11d812fd..2e43c732 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -632,6 +632,24 @@ }); }; + var opts = jQuery.extend({ + processVoteURL: '/process_vote', + addCommentURL: '/add_comment', + getCommentsURL: '/get_comments', + acceptCommentURL: '/accept_comment', + rejectCommentURL: '/reject_comment', + rejectCommentURL: '/delete_comment', + commentImage: '/static/_static/comment.png', + loadingImage: '/static/_static/ajax-loader.gif', + commentBrightImage: '/static/_static/comment-bright.png', + upArrow: '/static/_static/up.png', + downArrow: '/static/_static/down.png', + upArrowPressed: '/static/_static/up-pressed.png', + downArrowPressed: '/static/_static/down-pressed.png', + voting: false, + moderator: false + }, COMMENT_OPTIONS); + var replyTemplate = ' <li>\ <div class="reply_div" id="rd<%id%>">\ <form id="rf<%id%>">\ @@ -724,25 +742,6 @@ </div>\ <div id="focuser"></div>'; - - var opts = jQuery.extend({ - processVoteURL: '/process_vote', - addCommentURL: '/add_comment', - getCommentsURL: '/get_comments', - acceptCommentURL: '/accept_comment', - rejectCommentURL: '/reject_comment', - rejectCommentURL: '/delete_comment', - commentImage: '/static/_static/comment.png', - loadingImage: '/static/_static/comment.png', - commentBrightImage: '/static/_static/comment-bright.png', - upArrow: '/static/_static/up.png', - downArrow: '/static/_static/down.png', - upArrowPressed: '/static/_static/up-pressed.png', - downArrowPressed: '/static/_static/down-pressed.png', - voting: false, - moderator: false - }, COMMENT_OPTIONS); - $(document).ready(function() { init(); }); -- cgit v1.2.1 From 0c7788af5cb26c3fcfb623f812d4f67a95b7d2c3 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Tue, 10 Aug 2010 15:07:30 -0500 Subject: fix paths in js --- sphinx/websupport/__init__.py | 48 +++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 6fa9cf33..dc0361bd 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -354,29 +354,33 @@ class WebSupport(object): that remains the same throughout the lifetime of the :class:`~sphinx.websupport.WebSupport` object. """ - comment_urls = [ - ('addCommentURL', 'add_comment'), - ('getCommentsURL', 'get_comments'), - ('processVoteURL', 'process_vote'), - ('acceptCommentURL', 'accept_comment'), - ('rejectCommentURL', 'reject_comment'), - ('deleteCommentURL', 'delete_comment') - ] - static_urls = [ - ('commentImage', 'comment.png'), - ('loadingImage', 'ajax-loader.gif'), - ('commentBrightImage', 'comment-bright.png'), - ('upArrow', 'up.png'), - ('upArrowPressed', 'up-pressed.png'), - ('downArrow', 'down.png'), - ('downArrowPressed', 'down-pressed.png') - ] - self.base_comment_opts = {} - for key, value in comment_urls: - self.base_comment_opts[key] = posixpath.join(self.docroot, value) - for key, value in static_urls: - self.base_comment_opts[key] = posixpath.join(self.staticdir, value) + + if self.docroot is not '': + comment_urls = [ + ('addCommentURL', 'add_comment'), + ('getCommentsURL', 'get_comments'), + ('processVoteURL', 'process_vote'), + ('acceptCommentURL', 'accept_comment'), + ('rejectCommentURL', 'reject_comment'), + ('deleteCommentURL', 'delete_comment') + ] + for key, value in comment_urls: + self.base_comment_opts[key] = \ + '/' + posixpath.join(self.docroot, value) + if self.staticdir != 'static': + static_urls = [ + ('commentImage', 'comment.png'), + ('loadingImage', 'ajax-loader.gif'), + ('commentBrightImage', 'comment-bright.png'), + ('upArrow', 'up.png'), + ('upArrowPressed', 'up-pressed.png'), + ('downArrow', 'down.png'), + ('downArrowPressed', 'down-pressed.png') + ] + for key, value in static_urls: + self.base_comment_opts[key] = \ + '/' + posixpath.join(self.staticdir, '_static', value) def _make_comment_options(self, username, moderator): """Helper method to create the parts of the COMMENT_OPTIONS -- cgit v1.2.1 From 63686a628b4e89fcc306bbb8a7fb58539324bd59 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Tue, 10 Aug 2010 18:42:18 -0500 Subject: Don't need to convert moderator to string anymore --- sphinx/websupport/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index dc0361bd..cc065b7f 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -395,7 +395,7 @@ class WebSupport(object): rv.update({ 'voting': True, 'username': username, - 'moderator': str(moderator).lower(), + 'moderator': moderator, }) return '\n'.join([ '<script type="text/javascript">', -- cgit v1.2.1 From ca3a85ea42148b75b9cf8fde6a9db24a0da4ab91 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Tue, 10 Aug 2010 19:51:21 -0500 Subject: changed comment.png to faded image. --- sphinx/themes/basic/static/comment.png | Bin 3501 -> 3445 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/sphinx/themes/basic/static/comment.png b/sphinx/themes/basic/static/comment.png index bad742bb..92feb52b 100644 Binary files a/sphinx/themes/basic/static/comment.png and b/sphinx/themes/basic/static/comment.png differ -- cgit v1.2.1 From 72e7867ddb298fc015645d8ea0eb9f0a056e925a Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Wed, 11 Aug 2010 14:23:58 +0200 Subject: Added initial versioning support --- sphinx/builders/websupport.py | 38 ++++++++++++++++++++++++++ sphinx/websupport/storage/__init__.py | 4 ++- sphinx/websupport/storage/db.py | 8 ++++-- sphinx/websupport/storage/sqlalchemystorage.py | 4 +-- sphinx/writers/websupport.py | 3 +- tests/test_websupport.py | 26 ++++++++++-------- 6 files changed, 65 insertions(+), 18 deletions(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 8cc70ea0..e1bd8011 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -12,14 +12,23 @@ import cPickle as pickle from os import path from cgi import escape +from glob import glob +import os import posixpath import shutil + from docutils.io import StringOutput +from docutils.utils import Reporter 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 +from sphinx.environment import WarningStream +from sphinx.versioning import add_uids, merge_doctrees + +def is_paragraph(node): + return node.__class__.__name__ == 'paragraph' class WebSupportBuilder(StandaloneHTMLBuilder): """ @@ -28,13 +37,39 @@ class WebSupportBuilder(StandaloneHTMLBuilder): name = 'websupport' out_suffix = '.fpickle' + def init(self): + StandaloneHTMLBuilder.init(self) + for f in glob(path.join(self.doctreedir, '*.doctree')): + copyfile(f, f + '.old') + def init_translator_class(self): self.translator_class = WebSupportTranslator + def get_old_doctree(self, docname): + fp = self.env.doc2path(docname, self.doctreedir, '.doctree.old') + try: + f = open(fp, 'rb') + try: + doctree = pickle.load(f) + finally: + f.close() + except IOError: + return None + doctree.settings.env = self.env + doctree.reporter = Reporter(self.env.doc2path(docname), 2, 5, + stream=WarningStream(self.env._warnfunc)) + return doctree + def write_doc(self, docname, doctree): destination = StringOutput(encoding='utf-8') doctree.settings = self.docsettings + old_doctree = self.get_old_doctree(docname) + if old_doctree: + list(merge_doctrees(old_doctree, doctree, is_paragraph)) + else: + list(add_uids(doctree, is_paragraph)) + self.cur_docname = docname self.secnumbers = self.env.toc_secnumbers.get(docname, {}) self.imgpath = '/' + posixpath.join(self.app.staticdir, '_images') @@ -123,6 +158,9 @@ class WebSupportBuilder(StandaloneHTMLBuilder): shutil.move(path.join(self.outdir, '_static'), path.join(self.app.builddir, self.app.staticdir, '_static')) + for f in glob(path.join(self.doctreedir, '*.doctree.old')): + os.remove(f) + def dump_search_index(self): self.indexer.finish_indexing() diff --git a/sphinx/websupport/storage/__init__.py b/sphinx/websupport/storage/__init__.py index 17907e99..da815d0a 100644 --- a/sphinx/websupport/storage/__init__.py +++ b/sphinx/websupport/storage/__init__.py @@ -16,9 +16,11 @@ class StorageBackend(object): """ pass - def add_node(self, document, line, source): + def add_node(self, id, document, line, source): """Add a node to the StorageBackend. + :param id: a unique id for the comment. + :param document: the name of the document the node belongs to. :param line: the line in the source where the node begins. diff --git a/sphinx/websupport/storage/db.py b/sphinx/websupport/storage/db.py index 74a3e2b7..54b16f22 100644 --- a/sphinx/websupport/storage/db.py +++ b/sphinx/websupport/storage/db.py @@ -11,6 +11,7 @@ """ from datetime import datetime +from uuid import uuid4 from sqlalchemy import Column, Integer, Text, String, Boolean, ForeignKey,\ DateTime @@ -28,7 +29,7 @@ class Node(Base): """Data about a Node in a doctree.""" __tablename__ = db_prefix + 'nodes' - id = Column(Integer, primary_key=True) + id = Column(String(32), primary_key=True) document = Column(String(256), nullable=False) line = Column(Integer) source = Column(Text, nullable=False) @@ -93,7 +94,8 @@ class Node(Base): return comments - def __init__(self, document, line, source): + def __init__(self, id, document, line, source): + self.id = id self.document = document self.line = line self.source = source @@ -112,7 +114,7 @@ class Comment(Base): proposal_diff = Column(Text) path = Column(String(256), index=True) - node_id = Column(Integer, ForeignKey(db_prefix + 'nodes.id')) + node_id = Column(String, ForeignKey(db_prefix + 'nodes.id')) node = relation(Node, backref="comments") def __init__(self, text, displayed, username, rating, time, diff --git a/sphinx/websupport/storage/sqlalchemystorage.py b/sphinx/websupport/storage/sqlalchemystorage.py index e2cd87ac..d1683f60 100644 --- a/sphinx/websupport/storage/sqlalchemystorage.py +++ b/sphinx/websupport/storage/sqlalchemystorage.py @@ -33,8 +33,8 @@ class SQLAlchemyStorage(StorageBackend): def pre_build(self): self.build_session = Session() - def add_node(self, document, line, source): - node = Node(document, line, source) + def add_node(self, id, document, line, source): + node = Node(id, document, line, source) self.build_session.add(node) self.build_session.flush() return node diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index 05bc2c8b..306cfd86 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -55,7 +55,8 @@ class WebSupportTranslator(HTMLTranslator): def add_db_node(self, node): storage = self.builder.app.storage - db_node_id = storage.add_node(document=self.builder.cur_docname, + db_node_id = storage.add_node(id=node.uid, + document=self.builder.cur_docname, line=node.line, source=node.rawsource or node.astext()) return db_node_id diff --git a/tests/test_websupport.py b/tests/test_websupport.py index 32249976..3e784405 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -12,6 +12,8 @@ import os from StringIO import StringIO +from nose import SkipTest + from sphinx.websupport import WebSupport from sphinx.websupport.errors import * from sphinx.websupport.storage.differ import CombinedHtmlDiff @@ -79,10 +81,10 @@ def test_comments(support): # Create a displayed comment and a non displayed comment. comment = support.add_comment('First test comment', - node_id=str(first_node.id), + node_id=first_node.id, username='user_one') hidden_comment = support.add_comment('Hidden comment', - node_id=str(first_node.id), + node_id=first_node.id, displayed=False) # Make sure that comments can't be added to a comment where # displayed == False, since it could break the algorithm that @@ -96,11 +98,11 @@ def test_comments(support): parent_id=str(comment['id']), displayed=False) # Add a comment to another node to make sure it isn't returned later. support.add_comment('Second test comment', - node_id=str(second_node.id), + node_id=second_node.id, username='user_two') # Access the comments as a moderator. - data = support.get_data(str(first_node.id), moderator=True) + data = support.get_data(first_node.id, moderator=True) comments = data['comments'] children = comments[0]['children'] assert len(comments) == 2 @@ -109,7 +111,7 @@ def test_comments(support): assert children[1]['text'] == 'Hidden child test comment' # Access the comments without being a moderator. - data = support.get_data(str(first_node.id)) + data = support.get_data(first_node.id) comments = data['comments'] children = comments[0]['children'] assert len(comments) == 1 @@ -124,10 +126,10 @@ def test_voting(support): nodes = session.query(Node).all() node = nodes[0] - comment = support.get_data(str(node.id))['comments'][0] + comment = support.get_data(node.id)['comments'][0] def check_rating(val): - data = support.get_data(str(node.id)) + data = support.get_data(node.id) comment = data['comments'][0] assert comment['rating'] == val, '%s != %s' % (comment['rating'], val) @@ -156,13 +158,13 @@ def test_proposals(support): session = Session() node = session.query(Node).first() - data = support.get_data(str(node.id)) + data = support.get_data(node.id) source = data['source'] proposal = source[:5] + source[10:15] + 'asdf' + source[15:] comment = support.add_comment('Proposal comment', - node_id=str(node.id), + node_id=node.id, proposal=proposal) @@ -172,7 +174,7 @@ def test_user_delete_comments(support): session = Session() node = session.query(Node).first() session.close() - return support.get_data(str(node.id))['comments'][0] + return support.get_data(node.id)['comments'][0] comment = get_comment() assert comment['username'] == 'user_one' @@ -192,7 +194,7 @@ def test_moderator_delete_comments(support): session = Session() node = session.query(Node).first() session.close() - return support.get_data(str(node.id), moderator=True)['comments'][1] + return support.get_data(node.id, moderator=True)['comments'][1] comment = get_comment() support.delete_comment(comment['id'], username='user_two', @@ -228,6 +230,8 @@ def moderation_callback(comment): @with_support(moderation_callback=moderation_callback) def test_moderation(support): + raise SkipTest( + 'test is broken, relies on order of test execution and numeric ids') accepted = support.add_comment('Accepted Comment', node_id=3, displayed=False) rejected = support.add_comment('Rejected comment', node_id=3, -- cgit v1.2.1 From 7bc8d95bc039662fce961f199edc02efd2f10967 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Wed, 11 Aug 2010 17:16:20 +0200 Subject: Fix file name in manifest. --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 5e3104a8..cfc44c17 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,7 +7,7 @@ include TODO include babel.cfg include Makefile -include setup_distribute.py +include distribute_setup.py include sphinx-autogen.py include sphinx-build.py include sphinx-quickstart.py -- cgit v1.2.1 From a5ccf920770cf8316bc6840e1138383802e2be2f Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Wed, 11 Aug 2010 18:15:30 +0200 Subject: Fixing indentation etc. Note: We need a javascript styleguide --- sphinx/themes/basic/static/websupport.js | 254 +++++++++++++++---------------- 1 file changed, 124 insertions(+), 130 deletions(-) diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index 2e43c732..5ee59b84 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -5,15 +5,13 @@ $.fn.autogrow.resize(textarea); - $(textarea) - .focus(function() { - textarea.interval = setInterval(function() { - $.fn.autogrow.resize(textarea); - }, 500); - }) - .blur(function() { - clearInterval(textarea.interval); - }); + $(textarea).focus(function() { + textarea.interval = setInterval(function() { + $.fn.autogrow.resize(textarea); + }, 500); + }).blur(function() { + clearInterval(textarea.interval); + }); }); }; @@ -113,11 +111,11 @@ if (document.cookie.length > 0) { var start = document.cookie.indexOf('sortBy='); if (start != -1) { - start = start + 7; - var end = document.cookie.indexOf(";", start); - if (end == -1) - end = document.cookie.length; - by = unescape(document.cookie.substring(start, end)); + start = start + 7; + var end = document.cookie.indexOf(";", start); + if (end == -1) + end = document.cookie.length; + by = unescape(document.cookie.substring(start, end)); } } setComparator(by); @@ -132,12 +130,12 @@ // Reset the main comment form, and set the value of the parent input. $('form#comment_form') .find('textarea,input') - .removeAttr('disabled').end() + .removeAttr('disabled').end() .find('input[name="node"]') - .val(id).end() + .val(id).end() .find('textarea[name="proposal"]') - .val('') - .hide(); + .val('') + .hide(); // Position the popup and show it. var clientWidth = document.documentElement.clientWidth; @@ -145,12 +143,12 @@ $('div#focuser').fadeIn('fast'); $('div.popup_comment') .css({ - 'top': 100+$(window).scrollTop(), - 'left': clientWidth/2-popupWidth/2, - 'position': 'absolute' + 'top': 100+$(window).scrollTop(), + 'left': clientWidth/2-popupWidth/2, + 'position': 'absolute' }) .fadeIn('fast', function() { - getComments(id); + getComments(id); }); }; @@ -163,9 +161,9 @@ $('ul#comment_ul').empty(); $('h3#comment_notification').show(); $('form#comment_form').find('textarea') - .val('').end() - .find('textarea, input') - .removeAttr('disabled'); + .val('').end() + .find('textarea, input') + .removeAttr('disabled'); }); }; @@ -179,28 +177,27 @@ url: opts.getCommentsURL, data: {node: id}, success: function(data, textStatus, request) { - var ul = $('ul#comment_ul').hide(); - $('form#comment_form') - .find('textarea[name="proposal"]') - .data('source', data.source); - - if (data.comments.length == 0) { - ul.html('<li>No comments yet.</li>'); - commentListEmpty = true; - var speed = 100; - } - else { - // If there are comments, sort them and put them in the list. - var comments = sortComments(data.comments); - var speed = data.comments.length * 100; - appendComments(comments, ul); - commentListEmpty = false; - } - $('h3#comment_notification').slideUp(speed+200); - ul.slideDown(speed); + var ul = $('ul#comment_ul').hide(); + $('form#comment_form') + .find('textarea[name="proposal"]') + .data('source', data.source); + + if (data.comments.length == 0) { + ul.html('<li>No comments yet.</li>'); + commentListEmpty = true; + var speed = 100; + } else { + // If there are comments, sort them and put them in the list. + var comments = sortComments(data.comments); + var speed = data.comments.length * 100; + appendComments(comments, ul); + commentListEmpty = false; + } + $('h3#comment_notification').slideUp(speed+200); + ul.slideDown(speed); }, error: function(request, textStatus, error) { - showError('Oops, there was a problem retrieving the comments.'); + showError('Oops, there was a problem retrieving the comments.'); }, dataType: 'json' }); @@ -219,28 +216,30 @@ type: "POST", url: opts.addCommentURL, dataType: 'json', - data: {node: node_id, - parent: form.find('input[name="parent"]').val(), - text: form.find('textarea[name="comment"]').val(), - proposal: form.find('textarea[name="proposal"]').val()}, + data: { + node: node_id, + parent: form.find('input[name="parent"]').val(), + text: form.find('textarea[name="comment"]').val(), + proposal: form.find('textarea[name="proposal"]').val() + }, success: function(data, textStatus, error) { - // Reset the form. - if (node_id) { - hideProposeChange(node_id); - } - form.find('textarea') - .val('') - .add(form.find('input')) + // Reset the form. + if (node_id) { + hideProposeChange(node_id); + } + form.find('textarea') + .val('') + .add(form.find('input')) .removeAttr('disabled'); - if (commentListEmpty) { - $('ul#comment_ul').empty(); - commentListEmpty = false; - } - insertComment(data.comment); + if (commentListEmpty) { + $('ul#comment_ul').empty(); + commentListEmpty = false; + } + insertComment(data.comment); }, error: function(request, textStatus, error) { - form.find('textarea,input').removeAttr('disabled'); - showError('Oops, there was a problem adding the comment.'); + form.find('textarea,input').removeAttr('disabled'); + showError('Oops, there was a problem adding the comment.'); } }); }; @@ -274,8 +273,7 @@ if (comment.node != null) { var ul = $('ul#comment_ul'); var siblings = getChildren(ul); - } - else { + } else { var ul = $('#cl' + comment.parent); var siblings = getChildren(ul); } @@ -286,11 +284,11 @@ // Determine where in the parents children list to insert this comment. for(i=0; i < siblings.length; i++) { if (comp(comment, siblings[i]) <= 0) { - $('#cd' + siblings[i].id) - .parent() - .before(li.html(div)); - li.slideDown('fast'); - return; + $('#cd' + siblings[i].id) + .parent() + .before(li.html(div)); + li.slideDown('fast'); + return; } } @@ -306,10 +304,10 @@ url: opts.acceptCommentURL, data: {id: id}, success: function(data, textStatus, request) { - $('#cm' + id).fadeOut('fast'); + $('#cm' + id).fadeOut('fast'); }, error: function(request, textStatus, error) { - showError("Oops, there was a problem accepting the comment."); + showError("Oops, there was a problem accepting the comment."); }, }); }; @@ -320,13 +318,13 @@ url: opts.rejectCommentURL, data: {id: id}, success: function(data, textStatus, request) { - var div = $('#cd' + id); - div.slideUp('fast', function() { - div.remove(); - }); + var div = $('#cd' + id); + div.slideUp('fast', function() { + div.remove(); + }); }, error: function(request, textStatus, error) { - showError("Oops, there was a problem rejecting the comment."); + showError("Oops, there was a problem rejecting the comment."); }, }); }; @@ -337,22 +335,22 @@ url: opts.deleteCommentURL, data: {id: id}, success: function(data, textStatus, request) { - var div = $('#cd' + id); - div - .find('span.user_id:first') - .text('[deleted]').end() - .find('p.comment_text:first') - .text('[deleted]').end() - .find('#cm' + id + ', #dc' + id + ', #ac' + id + ', #rc' + id + - ', #sp' + id + ', #hp' + id + ', #cr' + id + ', #rl' + id) - .remove(); - var comment = div.data('comment'); - comment.username = '[deleted]'; - comment.text = '[deleted]'; - div.data('comment', comment); + var div = $('#cd' + id); + div + .find('span.user_id:first') + .text('[deleted]').end() + .find('p.comment_text:first') + .text('[deleted]').end() + .find('#cm' + id + ', #dc' + id + ', #ac' + id + ', #rc' + id + + ', #sp' + id + ', #hp' + id + ', #cr' + id + ', #rl' + id) + .remove(); + var comment = div.data('comment'); + comment.username = '[deleted]'; + comment.text = '[deleted]'; + div.data('comment', comment); }, error: function(request, textStatus, error) { - showError("Oops, there was a problem deleting the comment."); + showError("Oops, there was a problem deleting the comment."); }, }); }; @@ -437,10 +435,8 @@ // If this is not an unvote, and the other vote arrow has // already been pressed, unpress it. if ((d.value != 0) && (data.vote == d.value*-1)) { - $('#' + (d.value == 1 ? 'd' : 'u') + 'u' + d.comment_id) - .hide(); - $('#' + (d.value == 1 ? 'd' : 'u') + 'v' + d.comment_id) - .show(); + $('#' + (d.value == 1 ? 'd' : 'u') + 'u' + d.comment_id).hide(); + $('#' + (d.value == 1 ? 'd' : 'u') + 'v' + d.comment_id).show(); } // Update the comments rating in the local data. @@ -458,7 +454,7 @@ url: opts.processVoteURL, data: d, error: function(request, textStatus, error) { - showError("Oops, there was a problem casting that vote."); + showError("Oops, there was a problem casting that vote."); } }); }; @@ -477,12 +473,12 @@ .prepend(div) // Setup the submit handler for the reply form. .find('#rf' + id) - .submit(function(event) { - event.preventDefault(); - addComment($('#rf' + id)); - closeReply(id); - }); - div.slideDown('fast'); + .submit(function(event) { + event.preventDefault(); + addComment($('#rf' + id)); + closeReply(id); + }); + div.slideDown('fast'); }; /** @@ -520,10 +516,10 @@ if (by.substring(0,3) == 'asc') { var i = by.substring(3); comp = function(a, b) { return a[i] - b[i]; } - } - // Otherwise sort in descending order. - else + } else { + // Otherwise sort in descending order. comp = function(a, b) { return b[by] - a[by]; } + } // Reset link styles and format the selected sort option. $('a.sel').attr('href', '#').removeClass('sel'); @@ -536,15 +532,14 @@ */ function getChildren(ul, recursive) { var children = []; - ul.children().children("[id^='cd']") - .each(function() { - var comment = $(this).data('comment'); - if (recursive) { - comment.children = - getChildren($(this).find('#cl' + comment.id), true); - } - children.push(comment); - }); + ul.children().children("[id^='cd']").each(function() { + var comment = $(this).data('comment'); + if (recursive) { + comment.children = + getChildren($(this).find('#cl' + comment.id), true); + } + children.push(comment); + }); return children; }; @@ -569,16 +564,15 @@ if (comment.text != '[deleted]') { div.find('a.reply').show(); if (comment.proposal_diff) { - div.find('#sp' + comment.id).show(); + div.find('#sp' + comment.id).show(); } if (opts.moderator && !comment.displayed) { - div.find('#cm' + comment.id).show(); + div.find('#cm' + comment.id).show(); } if (opts.moderator || (opts.username == comment.username)) { - div.find('#dc' + comment.id).show(); + div.find('#dc' + comment.id).show(); } } - return div; } @@ -592,7 +586,7 @@ function handle(ph, escape) { var cur = context; $.each(ph.split('.'), function() { - cur = cur[this]; + cur = cur[this]; }); return escape ? esc.text(cur || "").html() : cur; } @@ -606,10 +600,10 @@ $('<div class="popup_error">' + '<h1>' + message + '</h1>' + '</div>') - .appendTo('body') - .fadeIn("slow") + .appendTo('body') + .fadeIn("slow") .delay(2000) - .fadeOut("slow"); + .fadeOut("slow"); }; /** @@ -622,13 +616,13 @@ var title = count + ' comment' + (count == 1 ? '' : 's'); var image = count > 0 ? opts.commentBrightImage : opts.commentImage; $(this).append( - $('<a href="#" class="sphinx_comment"></a>') - .html('<img src="' + image + '" alt="comment" />') - .attr('title', title) - .click(function(event) { - event.preventDefault(); - show($(this).parent().attr('id')); - })); + $('<a href="#" class="sphinx_comment"></a>') + .html('<img src="' + image + '" alt="comment" />') + .attr('title', title) + .click(function(event) { + event.preventDefault(); + show($(this).parent().attr('id')); + })); }); }; @@ -759,4 +753,4 @@ $(document).ready(function() { result.highlightText(this.toLowerCase(), 'highlighted'); }); }); -}); \ No newline at end of file +}); -- cgit v1.2.1 From 9b0e61e9b6f13d10417ee376e18f6d290da01c53 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Thu, 12 Aug 2010 02:54:54 +0200 Subject: Fallback to zip_longest for python3 --- sphinx/versioning.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sphinx/versioning.py b/sphinx/versioning.py index 42b8bd51..ef8effab 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -10,7 +10,11 @@ :license: BSD, see LICENSE for details. """ from uuid import uuid4 -from itertools import izip_longest, product +from itertools import product +try: + from itertools import izip_longest +except ImportError: + from itertools import zip_longest from difflib import SequenceMatcher from sphinx.util import PeekableIterator -- cgit v1.2.1 From 4fa32b0140331f06549ef9f8b50e4641fb2ef235 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Thu, 12 Aug 2010 03:09:03 +0200 Subject: Fixed NameError introduced by last changeset --- sphinx/versioning.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/versioning.py b/sphinx/versioning.py index ef8effab..d0ea18a7 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -12,7 +12,7 @@ from uuid import uuid4 from itertools import product try: - from itertools import izip_longest + from itertools import izip_longest as zip_longest except ImportError: from itertools import zip_longest from difflib import SequenceMatcher @@ -62,7 +62,7 @@ def merge_doctrees(old, new, condition): new_iter = PeekableIterator(new.traverse(condition)) old_nodes = [] new_nodes = [] - for old_node, new_node in izip_longest(old_iter, new_iter): + for old_node, new_node in zip_longest(old_iter, new_iter): if old_node is None: new_nodes.append(new_node) continue -- cgit v1.2.1 From bcd33cd916d8accde2713dda7ab4417bd5ef8c91 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Thu, 12 Aug 2010 14:41:25 -0500 Subject: allow commenting on literal_blocks --- sphinx/writers/websupport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index 05bc2c8b..c6516bf1 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -15,7 +15,7 @@ class WebSupportTranslator(HTMLTranslator): """ Our custom HTML translator. """ - commentable_nodes = ['paragraph'] + commentable_nodes = ['paragraph', 'literal_block'] def __init__(self, builder, *args, **kwargs): HTMLTranslator.__init__(self, builder, *args, **kwargs) -- cgit v1.2.1 From 0ec2f515aa8a2f9e33e9c3f03db8a4a1add253c2 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Thu, 12 Aug 2010 17:07:15 -0500 Subject: separate js templates from js scripts --- .../themes/basic/static/websupport-templates.html | 97 ++++++++++++++++ sphinx/themes/basic/static/websupport.js | 123 ++++----------------- sphinx/websupport/__init__.py | 3 +- 3 files changed, 122 insertions(+), 101 deletions(-) create mode 100644 sphinx/themes/basic/static/websupport-templates.html diff --git a/sphinx/themes/basic/static/websupport-templates.html b/sphinx/themes/basic/static/websupport-templates.html new file mode 100644 index 00000000..1ea58ab2 --- /dev/null +++ b/sphinx/themes/basic/static/websupport-templates.html @@ -0,0 +1,97 @@ +<div id="templates"> + <div id="reply_template"> + <Li> + <div class="reply_div" id="rd<%id%>"> + <form id="rf<%id%>"> + <textarea name="comment" cols="80"></textarea> + <input type="submit" value="add reply" /> + <input type="hidden" name="parent" value="<%id%>" /> + <input type="hidden" name="node" value="" /> + </form> + </div> + </li> + </div> + + <div id="comment_template"> + <div id="cd<%id%>" class="spxcdiv"> + <div class="vote"> + <div class="arrow"> + <a href="#" id="uv<%id%>" class="vote"> + <img src="<%upArrow%>" /> + </a> + <a href="#" id="uu<%id%>" class="un vote"> + <img src="<%upArrowPressed%>" /> + </a> + </div> + <div class="arrow"> + <a href="#" id="dv<%id%>" class="vote"> + <img src="<%downArrow%>" id="da<%id%>" /> + </a> + <a href="#" id="du<%id%>" class="un vote"> + <img src="<%downArrowPressed%>" /> + </a> + </div> + </div> + <div class="comment_content"> + <p class="tagline comment"> + <span class="user_id"><%username%></span> + <span class="rating"><%pretty_rating%></span> + <span class="delta"><%time.delta%></span> + </p> + <p class="comment_text comment"><%text%></p> + <p class="comment_opts comment"> + <a href="#" class="reply hidden" id="rl<%id%>">reply ▹</a> + <a href="#" class="close_reply" id="cr<%id%>">reply ▿</a> + <a href="#" id="sp<%id%>" class="show_proposal"> + proposal ▹ + </a> + <a href="#" id="hp<%id%>" class="hide_proposal"> + proposal ▿ + </a> + <a href="#" id="dc<%id%>" class="delete_comment hidden"> + delete + </a> + <span id="cm<%id%>" class="moderation hidden"> + <a href="#" id="ac<%id%>" class="accept_comment">accept</a> + <a href="#" id="rc<%id%>" class="reject_comment">reject</a> + </span> + </p> + <pre class="proposal" id="pr<%id%>"> + <#proposal_diff#> + </pre> + <ul class="children" id="cl<%id%>"></ul> + </div> + <div class="clearleft"></div> + </div> + </div> + + <div id="popup_template"> + <div class="popup_comment"> + <a id="comment_close" href="#">x</a> + <h1>Comments</h1> + <form method="post" id="comment_form" action="/docs/add_comment"> + <textarea name="comment" cols="80"></textarea> + <p class="propose_button"> + <a href="#" class="show_propose_change"> + Propose a change ▹ + </a> + <a href="#" class="hide_propose_change"> + Propose a change ▿ + </a> + </p> + <textarea name="proposal" cols="80" spellcheck="false"></textarea> + <input type="submit" value="add comment" id="comment_button" /> + <input type="hidden" name="node" /> + <input type="hidden" name="parent" value="" /> + <p class="sort_options"> + Sort by: + <a href="#" class="sort_option" id="rating">top</a> + <a href="#" class="sort_option" id="ascage">newest</a> + <a href="#" class="sort_option" id="age">oldest</a> + </p> + </form> + <h3 id="comment_notification">loading comments... <img src="<%loadingImage%>" alt="" /></h3> + <ul id="comment_ul"></ul> + </div> + </div> +</div> diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index 2e43c732..1f00ee46 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -31,11 +31,10 @@ })(jQuery); (function($) { - var commentListEmpty, popup, comp; + var commentListEmpty, popup, comp, commentTemplate, replyTemplate; function init() { initTemplates(); - initEvents(); initComparator(); }; @@ -95,12 +94,27 @@ }; function initTemplates() { - // Create our popup div, the same div is recycled each time comments - // are displayed. - popup = $(renderTemplate(popupTemplate, opts)); - // Setup autogrow on the textareas - popup.find('textarea').autogrow(); - $('body').append(popup); + var templateURL = opts.staticDir + '/_static/websupport-templates.html'; + $.get(templateURL, function(data) { + var templates = $(data); + function loadTemplate(id) { + var html = templates.find('#' + id).html(); + html = html.replace(/(<)|(%3C)/g, "<"); + html = html.replace(/(>)|(%3E)/g, ">"); + return html; + }; + // Create our popup div, the same div is recycled each time comments + // are displayed. + // Setup autogrow on the textareas + var popupTemplate = loadTemplate('popup_template'); + popup = $(renderTemplate(popupTemplate, opts)); + popup.find('textarea').autogrow(); + + commentTemplate = loadTemplate('#comment_template'); + replyTemplate = loadTemplate('#reply_template'); + $('body').append(popup); + initEvents(); + }); }; /** @@ -646,102 +660,11 @@ downArrow: '/static/_static/down.png', upArrowPressed: '/static/_static/up-pressed.png', downArrowPressed: '/static/_static/down-pressed.png', + staticDir: '/static', voting: false, moderator: false }, COMMENT_OPTIONS); - var replyTemplate = ' <li>\ - <div class="reply_div" id="rd<%id%>">\ - <form id="rf<%id%>">\ - <textarea name="comment" cols="80"></textarea>\ - <input type="submit" value="add reply" />\ - <input type="hidden" name="parent" value="<%id%>" />\ - <input type="hidden" name="node" value="" />\ - </form>\ - </div>\ - </li>'; - - var commentTemplate = ' <div id="cd<%id%>" class="spxcdiv">\ - <div class="vote">\ - <div class="arrow">\ - <a href="#" id="uv<%id%>" class="vote">\ - <img src="<%upArrow%>" />\ - </a>\ - <a href="#" id="uu<%id%>" class="un vote">\ - <img src="<%upArrowPressed%>" />\ - </a>\ - </div>\ - <div class="arrow">\ - <a href="#" id="dv<%id%>" class="vote">\ - <img src="<%downArrow%>" id="da<%id%>" />\ - </a>\ - <a href="#" id="du<%id%>" class="un vote">\ - <img src="<%downArrowPressed%>" />\ - </a>\ - </div>\ - </div>\ - <div class="comment_content">\ - <p class="tagline comment">\ - <span class="user_id"><%username%></span>\ - <span class="rating"><%pretty_rating%></span>\ - <span class="delta"><%time.delta%></span>\ - </p>\ - <p class="comment_text comment"><%text%></p>\ - <p class="comment_opts comment">\ - <a href="#" class="reply hidden" id="rl<%id%>">reply ▹</a>\ - <a href="#" class="close_reply" id="cr<%id%>">reply ▿</a>\ - <a href="#" id="sp<%id%>" class="show_proposal">\ - proposal ▹\ - </a>\ - <a href="#" id="hp<%id%>" class="hide_proposal">\ - proposal ▿\ - </a>\ - <a href="#" id="dc<%id%>" class="delete_comment hidden">\ - delete\ - </a>\ - <span id="cm<%id%>" class="moderation hidden">\ - <a href="#" id="ac<%id%>" class="accept_comment">accept</a>\ - <a href="#" id="rc<%id%>" class="reject_comment">reject</a>\ - </span>\ - </p>\ - <pre class="proposal" id="pr<%id%>">\ -<#proposal_diff#>\ - </pre>\ - <ul class="children" id="cl<%id%>"></ul>\ - </div>\ - <div class="clearleft"></div>\ - </div>'; - - var popupTemplate = ' <div class="popup_comment">\ - <a id="comment_close" href="#">x</a>\ - <h1>Comments</h1>\ - <form method="post" id="comment_form" action="/docs/add_comment">\ - <textarea name="comment" cols="80"></textarea>\ - <p class="propose_button">\ - <a href="#" class="show_propose_change">\ - Propose a change ▹\ - </a>\ - <a href="#" class="hide_propose_change">\ - Propose a change ▿\ - </a>\ - </p>\ - <textarea name="proposal" cols="80" spellcheck="false"></textarea>\ - <input type="submit" value="add comment" id="comment_button" />\ - <input type="hidden" name="node" />\ - <input type="hidden" name="parent" value="" />\ - <p class="sort_options">\ - Sort by:\ - <a href="#" class="sort_option" id="rating">top</a>\ - <a href="#" class="sort_option" id="ascage">newest</a>\ - <a href="#" class="sort_option" id="age">oldest</a>\ - </p>\ - </form>\ - <h3 id="comment_notification">loading comments... <img src="' + - opts.loadingImage + '" alt="" /></h3>\ - <ul id="comment_ul"></ul>\ - </div>\ - <div id="focuser"></div>'; - $(document).ready(function() { init(); }); diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index cc065b7f..939428a6 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -376,7 +376,8 @@ class WebSupport(object): ('upArrow', 'up.png'), ('upArrowPressed', 'up-pressed.png'), ('downArrow', 'down.png'), - ('downArrowPressed', 'down-pressed.png') + ('downArrowPressed', 'down-pressed.png'), + ('staticDir', '/' + self.staticdir) ] for key, value in static_urls: self.base_comment_opts[key] = \ -- cgit v1.2.1 From 7e0fd714177d1c02724c7040fec9980a42ced910 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Thu, 12 Aug 2010 17:08:27 -0500 Subject: remove modal focuser --- sphinx/themes/basic/static/websupport.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index 1f00ee46..80c6a9a5 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -156,7 +156,6 @@ // Position the popup and show it. var clientWidth = document.documentElement.clientWidth; var popupWidth = $('div.popup_comment').width(); - $('div#focuser').fadeIn('fast'); $('div.popup_comment') .css({ 'top': 100+$(window).scrollTop(), @@ -172,7 +171,6 @@ * Hide the comments popup window. */ function hide() { - $('div#focuser').fadeOut('fast'); $('div.popup_comment').fadeOut('fast', function() { $('ul#comment_ul').empty(); $('h3#comment_notification').show(); -- cgit v1.2.1 From 497b33ef8dce9d21ddf4d9d66d9c354e46bf466f Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Fri, 13 Aug 2010 11:37:21 +0200 Subject: Fix indentation --- .../themes/basic/static/websupport-templates.html | 132 ++++++++++----------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/sphinx/themes/basic/static/websupport-templates.html b/sphinx/themes/basic/static/websupport-templates.html index 1ea58ab2..9e7c0669 100644 --- a/sphinx/themes/basic/static/websupport-templates.html +++ b/sphinx/themes/basic/static/websupport-templates.html @@ -1,13 +1,13 @@ <div id="templates"> <div id="reply_template"> - <Li> + <li> <div class="reply_div" id="rd<%id%>"> - <form id="rf<%id%>"> - <textarea name="comment" cols="80"></textarea> + <form id="rf<%id%>"> + <textarea name="comment" cols="80"></textarea> <input type="submit" value="add reply" /> <input type="hidden" name="parent" value="<%id%>" /> <input type="hidden" name="node" value="" /> - </form> + </form> </div> </li> </div> @@ -15,51 +15,51 @@ <div id="comment_template"> <div id="cd<%id%>" class="spxcdiv"> <div class="vote"> - <div class="arrow"> - <a href="#" id="uv<%id%>" class="vote"> - <img src="<%upArrow%>" /> - </a> - <a href="#" id="uu<%id%>" class="un vote"> - <img src="<%upArrowPressed%>" /> - </a> - </div> - <div class="arrow"> - <a href="#" id="dv<%id%>" class="vote"> - <img src="<%downArrow%>" id="da<%id%>" /> - </a> - <a href="#" id="du<%id%>" class="un vote"> - <img src="<%downArrowPressed%>" /> - </a> - </div> + <div class="arrow"> + <a href="#" id="uv<%id%>" class="vote"> + <img src="<%upArrow%>" /> + </a> + <a href="#" id="uu<%id%>" class="un vote"> + <img src="<%upArrowPressed%>" /> + </a> + </div> + <div class="arrow"> + <a href="#" id="dv<%id%>" class="vote"> + <img src="<%downArrow%>" id="da<%id%>" /> + </a> + <a href="#" id="du<%id%>" class="un vote"> + <img src="<%downArrowPressed%>" /> + </a> + </div> </div> <div class="comment_content"> - <p class="tagline comment"> - <span class="user_id"><%username%></span> - <span class="rating"><%pretty_rating%></span> - <span class="delta"><%time.delta%></span> - </p> - <p class="comment_text comment"><%text%></p> - <p class="comment_opts comment"> - <a href="#" class="reply hidden" id="rl<%id%>">reply ▹</a> - <a href="#" class="close_reply" id="cr<%id%>">reply ▿</a> - <a href="#" id="sp<%id%>" class="show_proposal"> - proposal ▹ - </a> - <a href="#" id="hp<%id%>" class="hide_proposal"> - proposal ▿ - </a> - <a href="#" id="dc<%id%>" class="delete_comment hidden"> - delete - </a> - <span id="cm<%id%>" class="moderation hidden"> - <a href="#" id="ac<%id%>" class="accept_comment">accept</a> - <a href="#" id="rc<%id%>" class="reject_comment">reject</a> - </span> - </p> - <pre class="proposal" id="pr<%id%>"> - <#proposal_diff#> - </pre> - <ul class="children" id="cl<%id%>"></ul> + <p class="tagline comment"> + <span class="user_id"><%username%></span> + <span class="rating"><%pretty_rating%></span> + <span class="delta"><%time.delta%></span> + </p> + <p class="comment_text comment"><%text%></p> + <p class="comment_opts comment"> + <a href="#" class="reply hidden" id="rl<%id%>">reply ▹</a> + <a href="#" class="close_reply" id="cr<%id%>">reply ▿</a> + <a href="#" id="sp<%id%>" class="show_proposal"> + proposal ▹ + </a> + <a href="#" id="hp<%id%>" class="hide_proposal"> + proposal ▿ + </a> + <a href="#" id="dc<%id%>" class="delete_comment hidden"> + delete + </a> + <span id="cm<%id%>" class="moderation hidden"> + <a href="#" id="ac<%id%>" class="accept_comment">accept</a> + <a href="#" id="rc<%id%>" class="reject_comment">reject</a> + </span> + </p> + <pre class="proposal" id="pr<%id%>"> + <#proposal_diff#> + </pre> + <ul class="children" id="cl<%id%>"></ul> </div> <div class="clearleft"></div> </div> @@ -70,25 +70,25 @@ <a id="comment_close" href="#">x</a> <h1>Comments</h1> <form method="post" id="comment_form" action="/docs/add_comment"> - <textarea name="comment" cols="80"></textarea> - <p class="propose_button"> - <a href="#" class="show_propose_change"> - Propose a change ▹ - </a> - <a href="#" class="hide_propose_change"> - Propose a change ▿ - </a> - </p> - <textarea name="proposal" cols="80" spellcheck="false"></textarea> - <input type="submit" value="add comment" id="comment_button" /> - <input type="hidden" name="node" /> - <input type="hidden" name="parent" value="" /> - <p class="sort_options"> - Sort by: - <a href="#" class="sort_option" id="rating">top</a> - <a href="#" class="sort_option" id="ascage">newest</a> - <a href="#" class="sort_option" id="age">oldest</a> - </p> + <textarea name="comment" cols="80"></textarea> + <p class="propose_button"> + <a href="#" class="show_propose_change"> + Propose a change ▹ + </a> + <a href="#" class="hide_propose_change"> + Propose a change ▿ + </a> + </p> + <textarea name="proposal" cols="80" spellcheck="false"></textarea> + <input type="submit" value="add comment" id="comment_button" /> + <input type="hidden" name="node" /> + <input type="hidden" name="parent" value="" /> + <p class="sort_options"> + Sort by: + <a href="#" class="sort_option" id="rating">top</a> + <a href="#" class="sort_option" id="ascage">newest</a> + <a href="#" class="sort_option" id="age">oldest</a> + </p> </form> <h3 id="comment_notification">loading comments... <img src="<%loadingImage%>" alt="" /></h3> <ul id="comment_ul"></ul> -- cgit v1.2.1 From ecbbfd555f6d7b15f56a1245dceef13b39dc6cba Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Fri, 13 Aug 2010 11:44:09 +0200 Subject: Put literal_blocks under versioning --- sphinx/builders/websupport.py | 7 +++---- sphinx/util/websupport.py | 10 ++++++++++ sphinx/writers/websupport.py | 6 +++--- 3 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 sphinx/util/websupport.py diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index e1bd8011..2e05da5b 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -22,13 +22,12 @@ from docutils.utils import Reporter from sphinx.util.osutil import os_path, relative_uri, ensuredir, copyfile from sphinx.util.jsonimpl import dumps as dump_json +from sphinx.util.websupport import is_commentable from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.writers.websupport import WebSupportTranslator from sphinx.environment import WarningStream from sphinx.versioning import add_uids, merge_doctrees -def is_paragraph(node): - return node.__class__.__name__ == 'paragraph' class WebSupportBuilder(StandaloneHTMLBuilder): """ @@ -66,9 +65,9 @@ class WebSupportBuilder(StandaloneHTMLBuilder): old_doctree = self.get_old_doctree(docname) if old_doctree: - list(merge_doctrees(old_doctree, doctree, is_paragraph)) + list(merge_doctrees(old_doctree, doctree, is_commentable)) else: - list(add_uids(doctree, is_paragraph)) + list(add_uids(doctree, is_commentable)) self.cur_docname = docname self.secnumbers = self.env.toc_secnumbers.get(docname, {}) diff --git a/sphinx/util/websupport.py b/sphinx/util/websupport.py new file mode 100644 index 00000000..f99f4d31 --- /dev/null +++ b/sphinx/util/websupport.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +""" + sphinx.util.websupport + ~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" +def is_commentable(node): + return node.__class__.__name__ in ('paragraph', 'literal_block') diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index 84af925e..fbd3c1ef 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -10,12 +10,12 @@ """ from sphinx.writers.html import HTMLTranslator +from sphinx.util.websupport import is_commentable class WebSupportTranslator(HTMLTranslator): """ Our custom HTML translator. """ - commentable_nodes = ['paragraph', 'literal_block'] def __init__(self, builder, *args, **kwargs): HTMLTranslator.__init__(self, builder, *args, **kwargs) @@ -26,13 +26,13 @@ class WebSupportTranslator(HTMLTranslator): self.cur_node = None def dispatch_visit(self, node): - if node.__class__.__name__ in self.commentable_nodes: + if is_commentable(node): self.handle_visit_commentable(node) HTMLTranslator.dispatch_visit(self, node) def dispatch_departure(self, node): HTMLTranslator.dispatch_departure(self, node) - if node.__class__.__name__ in self.commentable_nodes: + if is_commentable(node): self.handle_depart_commentable(node) def handle_visit_commentable(self, node): -- cgit v1.2.1 From 96e4e72b7cb95e06d366e21ade224e3e77417216 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Fri, 13 Aug 2010 15:10:48 +0200 Subject: Use a more consistent style in the websupport js --- sphinx/themes/basic/static/websupport.js | 415 ++++++++++++++++--------------- 1 file changed, 208 insertions(+), 207 deletions(-) diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index 80c6a9a5..863a57a5 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -1,19 +1,19 @@ (function($) { $.fn.autogrow = function(){ return this.each(function(){ - var textarea = this; - - $.fn.autogrow.resize(textarea); - - $(textarea) - .focus(function() { - textarea.interval = setInterval(function() { - $.fn.autogrow.resize(textarea); - }, 500); - }) - .blur(function() { - clearInterval(textarea.interval); - }); + var textarea = this; + + $.fn.autogrow.resize(textarea); + + $(textarea) + .focus(function() { + textarea.interval = setInterval(function() { + $.fn.autogrow.resize(textarea); + }, 500); + }) + .blur(function() { + clearInterval(textarea.interval); + }); }); }; @@ -31,7 +31,7 @@ })(jQuery); (function($) { - var commentListEmpty, popup, comp, commentTemplate, replyTemplate; + var commentListEmpty, popup, comp, commentTemplate, replyTemplate; function init() { initTemplates(); @@ -98,9 +98,9 @@ $.get(templateURL, function(data) { var templates = $(data); function loadTemplate(id) { - var html = templates.find('#' + id).html(); - html = html.replace(/(<)|(%3C)/g, "<"); - html = html.replace(/(>)|(%3E)/g, ">"); + var html = templates.find('#' + id).html(); + html = html.replace(/(<)|(%3C)/g, "<"); + html = html.replace(/(>)|(%3E)/g, ">"); return html; }; // Create our popup div, the same div is recycled each time comments @@ -117,110 +117,109 @@ }); }; - /** - * Create a comp function. If the user has preferences stored in - * the sortBy cookie, use those, otherwise use the default. - */ + /* + Create a comp function. If the user has preferences stored in + the sortBy cookie, use those, otherwise use the default. + */ function initComparator() { var by = 'rating'; // Default to sort by rating. // If the sortBy cookie is set, use that instead. if (document.cookie.length > 0) { var start = document.cookie.indexOf('sortBy='); if (start != -1) { - start = start + 7; - var end = document.cookie.indexOf(";", start); - if (end == -1) - end = document.cookie.length; - by = unescape(document.cookie.substring(start, end)); - } + start = start + 7; + var end = document.cookie.indexOf(";", start); + if (end == -1) + end = document.cookie.length; + by = unescape(document.cookie.substring(start, end)); + } } setComparator(by); }; - /** - * Show the comments popup window. - */ + /* + Show the comments popup window. + */ function show(nodeId) { var id = nodeId.substring(1); // Reset the main comment form, and set the value of the parent input. $('form#comment_form') .find('textarea,input') - .removeAttr('disabled').end() + .removeAttr('disabled').end() .find('input[name="node"]') - .val(id).end() + .val(id).end() .find('textarea[name="proposal"]') - .val('') - .hide(); + .val('') + .hide(); // Position the popup and show it. var clientWidth = document.documentElement.clientWidth; var popupWidth = $('div.popup_comment').width(); $('div.popup_comment') .css({ - 'top': 100+$(window).scrollTop(), - 'left': clientWidth/2-popupWidth/2, - 'position': 'absolute' + 'top': 100+$(window).scrollTop(), + 'left': clientWidth/2-popupWidth/2, + 'position': 'absolute' }) .fadeIn('fast', function() { - getComments(id); + getComments(id); }); }; - /** - * Hide the comments popup window. - */ + /* + Hide the comments popup window. + */ function hide() { $('div.popup_comment').fadeOut('fast', function() { $('ul#comment_ul').empty(); $('h3#comment_notification').show(); $('form#comment_form').find('textarea') - .val('').end() - .find('textarea, input') - .removeAttr('disabled'); + .val('').end() + .find('textarea, input') + .removeAttr('disabled'); }); }; - /** - * Perform an ajax request to get comments for a node - * and insert the comments into the comments tree. - */ + /* + Perform an ajax request to get comments for a node + and insert the comments into the comments tree. + */ function getComments(id) { $.ajax({ - type: 'GET', - url: opts.getCommentsURL, - data: {node: id}, - success: function(data, textStatus, request) { - var ul = $('ul#comment_ul').hide(); - $('form#comment_form') - .find('textarea[name="proposal"]') - .data('source', data.source); - - if (data.comments.length == 0) { - ul.html('<li>No comments yet.</li>'); - commentListEmpty = true; - var speed = 100; - } - else { - // If there are comments, sort them and put them in the list. - var comments = sortComments(data.comments); - var speed = data.comments.length * 100; - appendComments(comments, ul); - commentListEmpty = false; - } - $('h3#comment_notification').slideUp(speed+200); - ul.slideDown(speed); - }, - error: function(request, textStatus, error) { - showError('Oops, there was a problem retrieving the comments.'); - }, - dataType: 'json' + type: 'GET', + url: opts.getCommentsURL, + data: {node: id}, + success: function(data, textStatus, request) { + var ul = $('ul#comment_ul').hide(); + $('form#comment_form') + .find('textarea[name="proposal"]') + .data('source', data.source); + + if (data.comments.length == 0) { + ul.html('<li>No comments yet.</li>'); + commentListEmpty = true; + var speed = 100; + } else { + // If there are comments, sort them and put them in the list. + var comments = sortComments(data.comments); + var speed = data.comments.length * 100; + appendComments(comments, ul); + commentListEmpty = false; + } + $('h3#comment_notification').slideUp(speed+200); + ul.slideDown(speed); + }, + error: function(request, textStatus, error) { + showError('Oops, there was a problem retrieving the comments.'); + }, + dataType: 'json' }); }; - /** - * Add a comment via ajax and insert the comment into the comment tree. - */ + /* + Add a comment via ajax and insert the comment into the comment tree. + */ function addComment(form) { // Disable the form that is being submitted. form.find('textarea,input').attr('disabled', 'disabled'); @@ -231,36 +230,38 @@ type: "POST", url: opts.addCommentURL, dataType: 'json', - data: {node: node_id, - parent: form.find('input[name="parent"]').val(), - text: form.find('textarea[name="comment"]').val(), - proposal: form.find('textarea[name="proposal"]').val()}, + data: { + node: node_id, + parent: form.find('input[name="parent"]').val(), + text: form.find('textarea[name="comment"]').val(), + proposal: form.find('textarea[name="proposal"]').val() + }, success: function(data, textStatus, error) { - // Reset the form. - if (node_id) { - hideProposeChange(node_id); - } - form.find('textarea') - .val('') - .add(form.find('input')) - .removeAttr('disabled'); - if (commentListEmpty) { - $('ul#comment_ul').empty(); - commentListEmpty = false; - } - insertComment(data.comment); + // Reset the form. + if (node_id) { + hideProposeChange(node_id); + } + form.find('textarea') + .val('') + .add(form.find('input')) + .removeAttr('disabled'); + if (commentListEmpty) { + $('ul#comment_ul').empty(); + commentListEmpty = false; + } + insertComment(data.comment); }, error: function(request, textStatus, error) { - form.find('textarea,input').removeAttr('disabled'); - showError('Oops, there was a problem adding the comment.'); + form.find('textarea,input').removeAttr('disabled'); + showError('Oops, there was a problem adding the comment.'); } }); }; - /** - * Recursively append comments to the main comment list and children - * lists, creating the comment tree. - */ + /* + Recursively append comments to the main comment list and children + lists, creating the comment tree. + */ function appendComments(comments, ul) { $.each(comments, function() { var div = createCommentDiv(this); @@ -272,10 +273,10 @@ }); }; - /** - * After adding a new comment, it must be inserted in the correct - * location in the comment tree. - */ + /* + After adding a new comment, it must be inserted in the correct + location in the comment tree. + */ function insertComment(comment) { var div = createCommentDiv(comment); @@ -286,8 +287,7 @@ if (comment.node != null) { var ul = $('ul#comment_ul'); var siblings = getChildren(ul); - } - else { + } else { var ul = $('#cl' + comment.parent); var siblings = getChildren(ul); } @@ -298,11 +298,11 @@ // Determine where in the parents children list to insert this comment. for(i=0; i < siblings.length; i++) { if (comp(comment, siblings[i]) <= 0) { - $('#cd' + siblings[i].id) - .parent() - .before(li.html(div)); - li.slideDown('fast'); - return; + $('#cd' + siblings[i].id) + .parent() + .before(li.html(div)); + li.slideDown('fast'); + return; } } @@ -318,10 +318,10 @@ url: opts.acceptCommentURL, data: {id: id}, success: function(data, textStatus, request) { - $('#cm' + id).fadeOut('fast'); + $('#cm' + id).fadeOut('fast'); }, error: function(request, textStatus, error) { - showError("Oops, there was a problem accepting the comment."); + showError("Oops, there was a problem accepting the comment."); }, }); }; @@ -332,13 +332,13 @@ url: opts.rejectCommentURL, data: {id: id}, success: function(data, textStatus, request) { - var div = $('#cd' + id); - div.slideUp('fast', function() { - div.remove(); - }); + var div = $('#cd' + id); + div.slideUp('fast', function() { + div.remove(); + }); }, error: function(request, textStatus, error) { - showError("Oops, there was a problem rejecting the comment."); + showError("Oops, there was a problem rejecting the comment."); }, }); }; @@ -349,22 +349,22 @@ url: opts.deleteCommentURL, data: {id: id}, success: function(data, textStatus, request) { - var div = $('#cd' + id); - div - .find('span.user_id:first') - .text('[deleted]').end() - .find('p.comment_text:first') - .text('[deleted]').end() - .find('#cm' + id + ', #dc' + id + ', #ac' + id + ', #rc' + id + - ', #sp' + id + ', #hp' + id + ', #cr' + id + ', #rl' + id) - .remove(); - var comment = div.data('comment'); - comment.username = '[deleted]'; - comment.text = '[deleted]'; - div.data('comment', comment); + var div = $('#cd' + id); + div + .find('span.user_id:first') + .text('[deleted]').end() + .find('p.comment_text:first') + .text('[deleted]').end() + .find('#cm' + id + ', #dc' + id + ', #ac' + id + ', #rc' + id + + ', #sp' + id + ', #hp' + id + ', #cr' + id + ', #rl' + id) + .remove(); + var comment = div.data('comment'); + comment.username = '[deleted]'; + comment.text = '[deleted]'; + div.data('comment', comment); }, error: function(request, textStatus, error) { - showError("Oops, there was a problem deleting the comment."); + showError("Oops, there was a problem deleting the comment."); }, }); }; @@ -398,25 +398,25 @@ textarea.slideUp('fast'); }; - /** - * Handle when the user clicks on a sort by link. - */ + /* + Handle when the user clicks on a sort by link. + */ function handleReSort(link) { setComparator(link.attr('id')); - // Save/update the sortBy cookie. + // Save/update the sortBy cookie. var expiration = new Date(); expiration.setDate(expiration.getDate() + 365); document.cookie= 'sortBy=' + escape(link.attr('id')) + - ';expires=' + expiration.toUTCString(); + ';expires=' + expiration.toUTCString(); var comments = getChildren($('ul#comment_ul'), true); comments = sortComments(comments); appendComments(comments, $('ul#comment_ul').empty()); }; - /** - * Function to process a vote when a user clicks an arrow. - */ + /* + Function to process a vote when a user clicks an arrow. + */ function handleVote(link) { if (!opts.voting) { showError("You'll need to login to vote."); @@ -426,11 +426,11 @@ var id = link.attr('id'); // If it is an unvote, the new vote value is 0, // Otherwise it's 1 for an upvote, or -1 for a downvote. - if (id.charAt(1) == 'u') + if (id.charAt(1) == 'u') { var value = 0; - else + } else { var value = id.charAt(0) == 'u' ? 1 : -1; - + } // The data to be sent to the server. var d = { comment_id: id.substring(2), @@ -450,9 +450,9 @@ // already been pressed, unpress it. if ((d.value != 0) && (data.vote == d.value*-1)) { $('#' + (d.value == 1 ? 'd' : 'u') + 'u' + d.comment_id) - .hide(); + .hide(); $('#' + (d.value == 1 ? 'd' : 'u') + 'v' + d.comment_id) - .show(); + .show(); } // Update the comments rating in the local data. @@ -470,14 +470,14 @@ url: opts.processVoteURL, data: d, error: function(request, textStatus, error) { - showError("Oops, there was a problem casting that vote."); + showError("Oops, there was a problem casting that vote."); } }); }; - /** - * Open a reply form used to reply to an existing comment. - */ + /* + Open a reply form used to reply to an existing comment. + */ function openReply(id) { // Swap out the reply link for the hide link $('#rl' + id).hide(); @@ -489,17 +489,17 @@ .prepend(div) // Setup the submit handler for the reply form. .find('#rf' + id) - .submit(function(event) { - event.preventDefault(); - addComment($('#rf' + id)); - closeReply(id); - }); + .submit(function(event) { + event.preventDefault(); + addComment($('#rf' + id)); + closeReply(id); + }); div.slideDown('fast'); }; - /** - * Close the reply form opened with openReply. - */ + /* + Close the reply form opened with openReply. + */ function closeReply(id) { // Remove the reply div from the DOM. $('#rd' + id).slideUp('fast', function() { @@ -511,9 +511,9 @@ $('#rl' + id).show(); }; - /** - * Recursively sort a tree of comments using the comp comparator. - */ + /* + Recursively sort a tree of comments using the comp comparator. + */ function sortComments(comments) { comments.sort(comp); $.each(comments, function() { @@ -522,51 +522,50 @@ return comments; }; - /** - * Set comp, which is a comparator function used for sorting and - * inserting comments into the list. - */ + /* + Set comp, which is a comparator function used for sorting and + inserting comments into the list. + */ function setComparator(by) { // If the first three letters are "asc", sort in ascending order // and remove the prefix. if (by.substring(0,3) == 'asc') { var i = by.substring(3); comp = function(a, b) { return a[i] - b[i]; } - } - // Otherwise sort in descending order. - else + } else { + // Otherwise sort in descending order. comp = function(a, b) { return b[by] - a[by]; } + } // Reset link styles and format the selected sort option. $('a.sel').attr('href', '#').removeClass('sel'); $('#' + by).removeAttr('href').addClass('sel'); }; - /** - * Get the children comments from a ul. If recursive is true, - * recursively include childrens' children. - */ + /* + Get the children comments from a ul. If recursive is true, + recursively include childrens' children. + */ function getChildren(ul, recursive) { var children = []; ul.children().children("[id^='cd']") .each(function() { - var comment = $(this).data('comment'); - if (recursive) { - comment.children = - getChildren($(this).find('#cl' + comment.id), true); - } - children.push(comment); + var comment = $(this).data('comment'); + if (recursive) { + comment.children = getChildren($(this).find('#cl' + comment.id), true); + } + children.push(comment); }); return children; }; - /** - * Create a div to display a comment in. - */ + /* + Create a div to display a comment in. + */ function createCommentDiv(comment) { // Prettify the comment rating. comment.pretty_rating = comment.rating + ' point' + - (comment.rating == 1 ? '' : 's'); + (comment.rating == 1 ? '' : 's'); // Create a div for this comment. var context = $.extend({}, opts, comment); var div = $(renderTemplate(commentTemplate, context)); @@ -581,30 +580,30 @@ if (comment.text != '[deleted]') { div.find('a.reply').show(); if (comment.proposal_diff) { - div.find('#sp' + comment.id).show(); + div.find('#sp' + comment.id).show(); } if (opts.moderator && !comment.displayed) { - div.find('#cm' + comment.id).show(); + div.find('#cm' + comment.id).show(); } if (opts.moderator || (opts.username == comment.username)) { - div.find('#dc' + comment.id).show(); + div.find('#dc' + comment.id).show(); } } return div; } - /** - * A simple template renderer. Placeholders such as <%id%> are replaced - * by context['id']. Items are always escaped. - */ + /* + A simple template renderer. Placeholders such as <%id%> are replaced + by context['id']. Items are always escaped. + */ function renderTemplate(template, context) { var esc = $('<span></span>'); function handle(ph, escape) { var cur = context; $.each(ph.split('.'), function() { - cur = cur[this]; + cur = cur[this]; }); return escape ? esc.text(cur || "").html() : cur; } @@ -617,16 +616,17 @@ function showError(message) { $('<div class="popup_error">' + '<h1>' + message + '</h1>' + - '</div>') - .appendTo('body') - .fadeIn("slow") - .delay(2000) - .fadeOut("slow"); + '</div>' + ) + .appendTo('body') + .fadeIn("slow") + .delay(2000) + .fadeOut("slow"); }; - /** - * Add a link the user uses to open the comments popup. - */ + /* + Add a link the user uses to open the comments popup. + */ $.fn.comment = function() { return this.each(function() { var id = $(this).attr('id').substring(1); @@ -634,13 +634,14 @@ var title = count + ' comment' + (count == 1 ? '' : 's'); var image = count > 0 ? opts.commentBrightImage : opts.commentImage; $(this).append( - $('<a href="#" class="sphinx_comment"></a>') - .html('<img src="' + image + '" alt="comment" />') - .attr('title', title) - .click(function(event) { - event.preventDefault(); - show($(this).parent().attr('id')); - })); + $('<a href="#" class="sphinx_comment"></a>') + .html('<img src="' + image + '" alt="comment" />') + .attr('title', title) + .click(function(event) { + event.preventDefault(); + show($(this).parent().attr('id')); + }) + ); }); }; @@ -680,4 +681,4 @@ $(document).ready(function() { result.highlightText(this.toLowerCase(), 'highlighted'); }); }); -}); \ No newline at end of file +}); -- cgit v1.2.1 From 82117268155794e8db195237856e740fcc398b57 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Fri, 13 Aug 2010 16:26:46 +0200 Subject: Put spaces around operators --- sphinx/themes/basic/static/websupport.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index 863a57a5..74e28c19 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -23,9 +23,9 @@ var columns = textarea.cols; var lineCount = 0; $.each(lines, function() { - lineCount += Math.ceil(this.length/columns) || 1; + lineCount += Math.ceil(this.length / columns) || 1; }); - var height = lineHeight*(lineCount+1); + var height = lineHeight * (lineCount + 1); $(textarea).css('height', height); }; })(jQuery); @@ -158,8 +158,8 @@ var popupWidth = $('div.popup_comment').width(); $('div.popup_comment') .css({ - 'top': 100+$(window).scrollTop(), - 'left': clientWidth/2-popupWidth/2, + 'top': 100 + $(window).scrollTop(), + 'left': clientWidth / 2 - popupWidth / 2, 'position': 'absolute' }) .fadeIn('fast', function() { @@ -207,7 +207,7 @@ appendComments(comments, ul); commentListEmpty = false; } - $('h3#comment_notification').slideUp(speed+200); + $('h3#comment_notification').slideUp(speed + 200); ul.slideDown(speed); }, error: function(request, textStatus, error) { @@ -448,11 +448,9 @@ // If this is not an unvote, and the other vote arrow has // already been pressed, unpress it. - if ((d.value != 0) && (data.vote == d.value*-1)) { - $('#' + (d.value == 1 ? 'd' : 'u') + 'u' + d.comment_id) - .hide(); - $('#' + (d.value == 1 ? 'd' : 'u') + 'v' + d.comment_id) - .show(); + if ((d.value != 0) && (data.vote == d.value * -1)) { + $('#' + (d.value == 1 ? 'd' : 'u') + 'u' + d.comment_id).hide(); + $('#' + (d.value == 1 ? 'd' : 'u') + 'v' + d.comment_id).show(); } // Update the comments rating in the local data. -- cgit v1.2.1 From ac632cfebadcf19504ae1982f7945fea6b640cbc Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Fri, 13 Aug 2010 17:07:15 +0200 Subject: Use document.createElement which is faster than parsing the html to create an element --- sphinx/themes/basic/static/websupport.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index 74e28c19..8405374d 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -265,7 +265,7 @@ function appendComments(comments, ul) { $.each(comments, function() { var div = createCommentDiv(this); - ul.append($('<li></li>').html(div)); + ul.append($(document.createElement('li')).html(div)); appendComments(this.children, div.find('ul.children')); // To avoid stagnating data, don't store the comments children in data. this.children = null; @@ -292,7 +292,7 @@ var siblings = getChildren(ul); } - var li = $('<li></li>'); + var li = $(document.createElement('li')); li.hide(); // Determine where in the parents children list to insert this comment. @@ -596,7 +596,7 @@ by context['id']. Items are always escaped. */ function renderTemplate(template, context) { - var esc = $('<span></span>'); + var esc = $(document.createElement('div')); function handle(ph, escape) { var cur = context; -- cgit v1.2.1 From 5f97290ce9ade9f9685bb97d11c204cae23a7379 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Fri, 13 Aug 2010 17:10:22 +0200 Subject: Implement showError more efficiently --- sphinx/themes/basic/static/websupport.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index 8405374d..079497a0 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -612,10 +612,8 @@ }; function showError(message) { - $('<div class="popup_error">' + - '<h1>' + message + '</h1>' + - '</div>' - ) + $(document.createElement('div').attr({class: 'popup_error'})) + .append($(document.createElement('h1').val(message))) .appendTo('body') .fadeIn("slow") .delay(2000) -- cgit v1.2.1 From 4cd1d03b674a8d3fdfe95ad4138b247bed9218b9 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Fri, 13 Aug 2010 17:17:40 +0200 Subject: Implement $.fn.comment more efficiently --- sphinx/themes/basic/static/websupport.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index 079497a0..cf0edf9b 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -630,9 +630,12 @@ var title = count + ' comment' + (count == 1 ? '' : 's'); var image = count > 0 ? opts.commentBrightImage : opts.commentImage; $(this).append( - $('<a href="#" class="sphinx_comment"></a>') - .html('<img src="' + image + '" alt="comment" />') - .attr('title', title) + $(document.createElement('a')).attr({href: '#', class: 'spinx_comment'}) + .append($(document.createElement('img')).attr({ + src: image, + alt: 'comment', + title: title + })) .click(function(event) { event.preventDefault(); show($(this).parent().attr('id')); -- cgit v1.2.1 From 5e862b3a3800ec7a8e1431b6f0d4226d6363f3d8 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Fri, 13 Aug 2010 17:28:37 +0200 Subject: Fixed showError --- sphinx/themes/basic/static/websupport.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index cf0edf9b..276d550b 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -612,8 +612,8 @@ }; function showError(message) { - $(document.createElement('div').attr({class: 'popup_error'})) - .append($(document.createElement('h1').val(message))) + $(document.createElement('div')).attr({class: 'popup_error'}) + .append($(document.createElement('h1')).text(message)) .appendTo('body') .fadeIn("slow") .delay(2000) -- cgit v1.2.1 From 498218b0c551c66365550b6c245e8161706ef570 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Fri, 13 Aug 2010 12:15:12 -0500 Subject: fixed bug in CombinedHtmlDiffer that clipped the last line. --- sphinx/themes/basic/static/websupport-templates.html | 2 +- sphinx/websupport/storage/differ.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx/themes/basic/static/websupport-templates.html b/sphinx/themes/basic/static/websupport-templates.html index 1ea58ab2..b28ec964 100644 --- a/sphinx/themes/basic/static/websupport-templates.html +++ b/sphinx/themes/basic/static/websupport-templates.html @@ -57,7 +57,7 @@ </span> </p> <pre class="proposal" id="pr<%id%>"> - <#proposal_diff#> +<#proposal_diff#> </pre> <ul class="children" id="cl<%id%>"></ul> </div> diff --git a/sphinx/websupport/storage/differ.py b/sphinx/websupport/storage/differ.py index f0b6a8ea..8d6c4a49 100644 --- a/sphinx/websupport/storage/differ.py +++ b/sphinx/websupport/storage/differ.py @@ -40,9 +40,9 @@ class CombinedHtmlDiff(object): try: next = diff.pop(0) except IndexError: - self._handle_line(line) + html.append(self._handle_line(line)) break - return ''.join(html) + return ''.join(html).rstrip() def _handle_line(self, line, next=None): """Handle an individual line in a diff.""" -- cgit v1.2.1 From 8685cea25307641bf5927b98f54c99cb6e641cbb Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Fri, 13 Aug 2010 14:32:30 -0500 Subject: readd modal focuser --- sphinx/themes/basic/static/websupport-templates.html | 1 + sphinx/themes/basic/static/websupport.js | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/sphinx/themes/basic/static/websupport-templates.html b/sphinx/themes/basic/static/websupport-templates.html index b28ec964..22bbaa41 100644 --- a/sphinx/themes/basic/static/websupport-templates.html +++ b/sphinx/themes/basic/static/websupport-templates.html @@ -94,4 +94,5 @@ <ul id="comment_ul"></ul> </div> </div> + <div id="focuser"></div> </div> diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index 276d550b..0d8fab41 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -110,9 +110,11 @@ popup = $(renderTemplate(popupTemplate, opts)); popup.find('textarea').autogrow(); - commentTemplate = loadTemplate('#comment_template'); - replyTemplate = loadTemplate('#reply_template'); + commentTemplate = loadTemplate('comment_template'); + replyTemplate = loadTemplate('reply_template'); + var focuser = templates.find('#focuser'); $('body').append(popup); + $('body').append(focuser); initEvents(); }); }; @@ -156,6 +158,7 @@ // Position the popup and show it. var clientWidth = document.documentElement.clientWidth; var popupWidth = $('div.popup_comment').width(); + $('div#focuser').fadeIn('fast'); $('div.popup_comment') .css({ 'top': 100 + $(window).scrollTop(), @@ -171,6 +174,7 @@ Hide the comments popup window. */ function hide() { + $('div#focuser').fadeOut('fast'); $('div.popup_comment').fadeOut('fast', function() { $('ul#comment_ul').empty(); $('h3#comment_notification').show(); -- cgit v1.2.1 From 2223275b88cd56ccb59e2721610726b8ab276c0e Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Fri, 13 Aug 2010 16:27:02 -0500 Subject: moved templates back into js, getting the html template file trys to load the images from template tags resulting in a bunch of 404's. This is a project for after pencils down date since it functions fine as is --- .../themes/basic/static/websupport-templates.html | 98 --------------- sphinx/themes/basic/static/websupport.js | 132 +++++++++++++++++---- sphinx/websupport/__init__.py | 3 +- 3 files changed, 108 insertions(+), 125 deletions(-) delete mode 100644 sphinx/themes/basic/static/websupport-templates.html diff --git a/sphinx/themes/basic/static/websupport-templates.html b/sphinx/themes/basic/static/websupport-templates.html deleted file mode 100644 index 22bbaa41..00000000 --- a/sphinx/themes/basic/static/websupport-templates.html +++ /dev/null @@ -1,98 +0,0 @@ -<div id="templates"> - <div id="reply_template"> - <Li> - <div class="reply_div" id="rd<%id%>"> - <form id="rf<%id%>"> - <textarea name="comment" cols="80"></textarea> - <input type="submit" value="add reply" /> - <input type="hidden" name="parent" value="<%id%>" /> - <input type="hidden" name="node" value="" /> - </form> - </div> - </li> - </div> - - <div id="comment_template"> - <div id="cd<%id%>" class="spxcdiv"> - <div class="vote"> - <div class="arrow"> - <a href="#" id="uv<%id%>" class="vote"> - <img src="<%upArrow%>" /> - </a> - <a href="#" id="uu<%id%>" class="un vote"> - <img src="<%upArrowPressed%>" /> - </a> - </div> - <div class="arrow"> - <a href="#" id="dv<%id%>" class="vote"> - <img src="<%downArrow%>" id="da<%id%>" /> - </a> - <a href="#" id="du<%id%>" class="un vote"> - <img src="<%downArrowPressed%>" /> - </a> - </div> - </div> - <div class="comment_content"> - <p class="tagline comment"> - <span class="user_id"><%username%></span> - <span class="rating"><%pretty_rating%></span> - <span class="delta"><%time.delta%></span> - </p> - <p class="comment_text comment"><%text%></p> - <p class="comment_opts comment"> - <a href="#" class="reply hidden" id="rl<%id%>">reply ▹</a> - <a href="#" class="close_reply" id="cr<%id%>">reply ▿</a> - <a href="#" id="sp<%id%>" class="show_proposal"> - proposal ▹ - </a> - <a href="#" id="hp<%id%>" class="hide_proposal"> - proposal ▿ - </a> - <a href="#" id="dc<%id%>" class="delete_comment hidden"> - delete - </a> - <span id="cm<%id%>" class="moderation hidden"> - <a href="#" id="ac<%id%>" class="accept_comment">accept</a> - <a href="#" id="rc<%id%>" class="reject_comment">reject</a> - </span> - </p> - <pre class="proposal" id="pr<%id%>"> -<#proposal_diff#> - </pre> - <ul class="children" id="cl<%id%>"></ul> - </div> - <div class="clearleft"></div> - </div> - </div> - - <div id="popup_template"> - <div class="popup_comment"> - <a id="comment_close" href="#">x</a> - <h1>Comments</h1> - <form method="post" id="comment_form" action="/docs/add_comment"> - <textarea name="comment" cols="80"></textarea> - <p class="propose_button"> - <a href="#" class="show_propose_change"> - Propose a change ▹ - </a> - <a href="#" class="hide_propose_change"> - Propose a change ▿ - </a> - </p> - <textarea name="proposal" cols="80" spellcheck="false"></textarea> - <input type="submit" value="add comment" id="comment_button" /> - <input type="hidden" name="node" /> - <input type="hidden" name="parent" value="" /> - <p class="sort_options"> - Sort by: - <a href="#" class="sort_option" id="rating">top</a> - <a href="#" class="sort_option" id="ascage">newest</a> - <a href="#" class="sort_option" id="age">oldest</a> - </p> - </form> - <h3 id="comment_notification">loading comments... <img src="<%loadingImage%>" alt="" /></h3> - <ul id="comment_ul"></ul> - </div> - </div> - <div id="focuser"></div> -</div> diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index 0d8fab41..a0aa7917 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -31,10 +31,11 @@ })(jQuery); (function($) { - var commentListEmpty, popup, comp, commentTemplate, replyTemplate; + var commentListEmpty, popup, comp; function init() { initTemplates(); + initEvents(); initComparator(); }; @@ -94,29 +95,12 @@ }; function initTemplates() { - var templateURL = opts.staticDir + '/_static/websupport-templates.html'; - $.get(templateURL, function(data) { - var templates = $(data); - function loadTemplate(id) { - var html = templates.find('#' + id).html(); - html = html.replace(/(<)|(%3C)/g, "<"); - html = html.replace(/(>)|(%3E)/g, ">"); - return html; - }; - // Create our popup div, the same div is recycled each time comments - // are displayed. - // Setup autogrow on the textareas - var popupTemplate = loadTemplate('popup_template'); - popup = $(renderTemplate(popupTemplate, opts)); - popup.find('textarea').autogrow(); - - commentTemplate = loadTemplate('comment_template'); - replyTemplate = loadTemplate('reply_template'); - var focuser = templates.find('#focuser'); - $('body').append(popup); - $('body').append(focuser); - initEvents(); - }); + // Create our popup div, the same div is recycled each time comments + // are displayed. + popup = $(renderTemplate(popupTemplate, opts)); + // Setup autogrow on the textareas + popup.find('textarea').autogrow(); + $('body').append(popup); }; /* @@ -662,11 +646,109 @@ downArrow: '/static/_static/down.png', upArrowPressed: '/static/_static/up-pressed.png', downArrowPressed: '/static/_static/down-pressed.png', - staticDir: '/static', voting: false, moderator: false }, COMMENT_OPTIONS); + var replyTemplate = '\ + <li>\ + <div class="reply_div" id="rd<%id%>">\ + <form id="rf<%id%>">\ + <textarea name="comment" cols="80"></textarea>\ + <input type="submit" value="add reply" />\ + <input type="hidden" name="parent" value="<%id%>" />\ + <input type="hidden" name="node" value="" />\ + </form>\ + </div>\ + </li>'; + + var commentTemplate = '\ + <div id="cd<%id%>" class="spxcdiv">\ + <div class="vote">\ + <div class="arrow">\ + <a href="#" id="uv<%id%>" class="vote">\ + <img src="<%upArrow%>" />\ + </a>\ + <a href="#" id="uu<%id%>" class="un vote">\ + <img src="<%upArrowPressed%>" />\ + </a>\ + </div>\ + <div class="arrow">\ + <a href="#" id="dv<%id%>" class="vote">\ + <img src="<%downArrow%>" id="da<%id%>" />\ + </a>\ + <a href="#" id="du<%id%>" class="un vote">\ + <img src="<%downArrowPressed%>" />\ + </a>\ + </div>\ + </div>\ + <div class="comment_content">\ + <p class="tagline comment">\ + <span class="user_id"><%username%></span>\ + <span class="rating"><%pretty_rating%></span>\ + <span class="delta"><%time.delta%></span>\ + </p>\ + <p class="comment_text comment"><%text%></p>\ + <p class="comment_opts comment">\ + <a href="#" class="reply hidden" id="rl<%id%>">reply ▹</a>\ + <a href="#" class="close_reply" id="cr<%id%>">reply ▿</a>\ + <a href="#" id="sp<%id%>" class="show_proposal">\ + proposal ▹\ + </a>\ + <a href="#" id="hp<%id%>" class="hide_proposal">\ + proposal ▿\ + </a>\ + <a href="#" id="dc<%id%>" class="delete_comment hidden">\ + delete\ + </a>\ + <span id="cm<%id%>" class="moderation hidden">\ + <a href="#" id="ac<%id%>" class="accept_comment">accept</a>\ + <a href="#" id="rc<%id%>" class="reject_comment">reject</a>\ + </span>\ + </p>\ + <pre class="proposal" id="pr<%id%>">\ +<#proposal_diff#>\ + </pre>\ + <ul class="children" id="cl<%id%>"></ul>\ + </div>\ + <div class="clearleft"></div>\ + </div>\ + </div>'; + + var popupTemplate = '\ + <div id="popup_template">\ + <div class="popup_comment">\ + <a id="comment_close" href="#">x</a>\ + <h1>Comments</h1>\ + <form method="post" id="comment_form" action="/docs/add_comment">\ + <textarea name="comment" cols="80"></textarea>\ + <p class="propose_button">\ + <a href="#" class="show_propose_change">\ + Propose a change ▹\ + </a>\ + <a href="#" class="hide_propose_change">\ + Propose a change ▿\ + </a>\ + </p>\ + <textarea name="proposal" cols="80" spellcheck="false"></textarea>\ + <input type="submit" value="add comment" id="comment_button" />\ + <input type="hidden" name="node" />\ + <input type="hidden" name="parent" value="" />\ + <p class="sort_options">\ + Sort by:\ + <a href="#" class="sort_option" id="rating">top</a>\ + <a href="#" class="sort_option" id="ascage">newest</a>\ + <a href="#" class="sort_option" id="age">oldest</a>\ + </p>\ + </form>\ + <h3 id="comment_notification">loading comments... <img src="' + + opts.loadingImage + '" alt="" /></h3>\ + <ul id="comment_ul"></ul>\ + </div>\ + </div>\ + <div id="focuser"></div>'; + + $(document).ready(function() { init(); }); diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 939428a6..cc065b7f 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -376,8 +376,7 @@ class WebSupport(object): ('upArrow', 'up.png'), ('upArrowPressed', 'up-pressed.png'), ('downArrow', 'down.png'), - ('downArrowPressed', 'down-pressed.png'), - ('staticDir', '/' + self.staticdir) + ('downArrowPressed', 'down-pressed.png') ] for key, value in static_urls: self.base_comment_opts[key] = \ -- cgit v1.2.1 From c0e116c026aebb10854d8e083e702f6290403116 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sat, 14 Aug 2010 11:11:50 +0200 Subject: Added a test to make sure pickled doctrees still have their uids --- tests/test_versioning.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 77306580..54a48f4a 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -8,10 +8,14 @@ :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ +import pickle + from util import * from docutils.statemachine import ViewList +from docutils.parsers.rst.directives.html import MetaBody +from sphinx import addnodes from sphinx.versioning import make_diff, add_uids, merge_doctrees def setup_module(): @@ -50,6 +54,19 @@ def is_paragraph(node): def test_add_uids(): assert len(original_uids) == 3 +def test_picklablility(): + # we have to modify the doctree so we can pickle it + copy = original.copy() + copy.reporter = None + copy.transformer = None + copy.settings.warning_stream = None + copy.settings.env = None + copy.settings.record_dependencies = None + for metanode in copy.traverse(MetaBody.meta): + metanode.__class__ = addnodes.meta + loaded = pickle.loads(pickle.dumps(copy, pickle.HIGHEST_PROTOCOL)) + assert all(getattr(n, 'uid', False) for n in loaded.traverse(is_paragraph)) + def test_modified(): modified = doctrees['versioning/modified'] new_nodes = list(merge_doctrees(original, modified, is_paragraph)) -- cgit v1.2.1 From dbfb39a182421e801537f2bd5bd97347bedf81b9 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sat, 14 Aug 2010 11:17:38 +0200 Subject: Fix finish handler of the websupport builder --- sphinx/builders/websupport.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 16e3a82b..0c92c646 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -151,12 +151,15 @@ class WebSupportBuilder(StandaloneHTMLBuilder): def handle_finish(self): StandaloneHTMLBuilder.handle_finish(self) - shutil.move(path.join(self.outdir, '_images'), - path.join(self.app.builddir, self.app.staticdir, - '_images')) - shutil.move(path.join(self.outdir, '_static'), - path.join(self.app.builddir, self.app.staticdir, - '_static')) + directories = ['_images', '_static'] + for directory in directories: + try: + shutil.move(path.join(self.outdir, directory), + path.join(self.app.builddir, self.app.staticdir, + directory)) + except IOError: + # in case any of these directories don't exist + pass for f in glob(path.join(self.doctreedir, '*.doctree.old')): os.remove(f) -- cgit v1.2.1 From 49241ecde13156fdb769313a83dc8ef5d93c9960 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sat, 14 Aug 2010 11:22:40 +0200 Subject: Use the highest protocol to pickle doctrees and use the constants provided by the pickle module --- sphinx/builders/websupport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 0c92c646..e43c46de 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -137,7 +137,7 @@ class WebSupportBuilder(StandaloneHTMLBuilder): ensuredir(path.dirname(outfilename)) f = open(outfilename, 'wb') try: - pickle.dump(doc_ctx, f, 2) + pickle.dump(doc_ctx, f, pickle.HIGHEST_PROTOCOL) finally: f.close() -- cgit v1.2.1 From fa978b05d7b962c240386dc1b540478fe31f75c3 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sat, 14 Aug 2010 12:16:40 +0200 Subject: Removed unnecessary newline --- sphinx/builders/websupport.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index e43c46de..5f27b907 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -163,7 +163,6 @@ class WebSupportBuilder(StandaloneHTMLBuilder): for f in glob(path.join(self.doctreedir, '*.doctree.old')): os.remove(f) - def dump_search_index(self): self.indexer.finish_indexing() -- cgit v1.2.1 From 02175d5f3116b0c61ba23bfdc9de6657956a707f Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 14 Aug 2010 14:31:20 +0200 Subject: Fix typo. --- sphinx/themes/basic/static/websupport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index a0aa7917..2cd70e9b 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -618,7 +618,7 @@ var title = count + ' comment' + (count == 1 ? '' : 's'); var image = count > 0 ? opts.commentBrightImage : opts.commentImage; $(this).append( - $(document.createElement('a')).attr({href: '#', class: 'spinx_comment'}) + $(document.createElement('a')).attr({href: '#', class: 'sphinx_comment'}) .append($(document.createElement('img')).attr({ src: image, alt: 'comment', -- cgit v1.2.1 From c550620cd96b50b696f0f554a4b87d06e87e51a3 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sat, 14 Aug 2010 14:43:54 +0200 Subject: Take doctrees in subdirectories into account --- sphinx/builders/websupport.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 5f27b907..ac9dd715 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -12,7 +12,6 @@ import cPickle as pickle from os import path from cgi import escape -from glob import glob import os import posixpath import shutil @@ -38,8 +37,11 @@ class WebSupportBuilder(StandaloneHTMLBuilder): def init(self): StandaloneHTMLBuilder.init(self) - for f in glob(path.join(self.doctreedir, '*.doctree')): - copyfile(f, f + '.old') + for root, dirs, files in os.walk(self.doctreedir): + for fn in files: + fp = path.join(root, fn) + if fp.endswith('.doctree'): + copyfile(fp, fp + '.old') def init_translator_class(self): self.translator_class = WebSupportTranslator @@ -160,8 +162,11 @@ class WebSupportBuilder(StandaloneHTMLBuilder): except IOError: # in case any of these directories don't exist pass - for f in glob(path.join(self.doctreedir, '*.doctree.old')): - os.remove(f) + for root, dirs, files in os.walk(self.doctreedir): + for fn in files: + fp = path.join(root, fn) + if fp.endswith('.doctree.old'): + os.remove(fp) def dump_search_index(self): self.indexer.finish_indexing() -- cgit v1.2.1 From bc1e0331af4834f0dd4f026fe3050bf5b9e9ece8 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sat, 14 Aug 2010 19:52:04 +0200 Subject: Fix doctest to work with Python 2.5 and lower --- tests/root/doctest.txt | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/tests/root/doctest.txt b/tests/root/doctest.txt index 6ac0b286..ba9a72c5 100644 --- a/tests/root/doctest.txt +++ b/tests/root/doctest.txt @@ -50,23 +50,24 @@ Special directives .. testsetup:: * - from math import factorial + def squared(x): + return x * x .. doctest:: - >>> factorial(1) - 1 + >>> squared(2) + 4 .. testcode:: - print(factorial(1)) + print(squared(2)) .. testoutput:: - 1 + 4 - >>> factorial(1) - 1 + >>> squared(2) + 4 * options for testcode/testoutput blocks @@ -85,36 +86,38 @@ Special directives .. testsetup:: group1 - from math import trunc + def add(x, y): + return x + y - ``trunc`` is now known in "group1", but not in others. + + ``add`` is now known in "group1", but not in others. .. doctest:: group1 - >>> trunc(1.1) - 1 + >>> add(1, 1) + 2 .. doctest:: group2 - >>> trunc(1.1) + >>> add(1, 1) Traceback (most recent call last): ... - NameError: name 'trunc' is not defined + NameError: name 'add' is not defined Interleaving testcode/testoutput: .. testcode:: group1 - print(factorial(3)) + print(squared(3)) .. testcode:: group2 - print(factorial(4)) + print(squared(4)) .. testoutput:: group1 - 6 + 9 .. testoutput:: group2 - 24 + 16 -- cgit v1.2.1 From 9278752f495e35c134609bd77cd98ee9c67b348a Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sat, 14 Aug 2010 19:48:42 +0200 Subject: shutil.copytree doesn't have an ignore argument in Python 2.4 --- tests/path.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/path.py b/tests/path.py index df96bce4..8e9afeaa 100644 --- a/tests/path.py +++ b/tests/path.py @@ -88,7 +88,7 @@ class path(str): """ shutil.rmtree(self, ignore_errors=ignore_errors, onerror=onerror) - def copytree(self, destination, symlinks=False, ignore=None): + def copytree(self, destination, symlinks=False): """ Recursively copy a directory to the given `destination`. If the given `destination` does not exist it will be created. @@ -97,12 +97,8 @@ class path(str): If ``True`` symbolic links in the source tree result in symbolic links in the destination tree otherwise the contents of the files pointed to by the symbolic links are copied. - - :param ignore: - A callback which gets called with the path of the directory being - copied and a list of paths as returned by :func:`os.listdir`. """ - shutil.copytree(self, destination, symlinks=symlinks, ignore=ignore) + shutil.copytree(self, destination, symlinks=symlinks) def movetree(self, destination): """ -- cgit v1.2.1 From c62aa7aa23f516f427498a88edc3a49748bc920b Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 14 Aug 2010 14:48:27 -0500 Subject: fix syntax highlighting in quickstart --- doc/web/quickstart.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/doc/web/quickstart.rst b/doc/web/quickstart.rst index de9b7655..61a432a8 100644 --- a/doc/web/quickstart.rst +++ b/doc/web/quickstart.rst @@ -143,8 +143,7 @@ will then add this data to the COMMENT_OPTIONS that are used in the template. need to prefix the url route with that directory, and give the `docroot` keyword argument when creating the web support object:: - support = WebSupport(... - docroot='docs') + support = WebSupport(..., docroot='docs') @app.route('/docs/<path:docname>') @@ -259,10 +258,9 @@ is added but not displayed, you can pass callable to the object:: def moderation_callback(comment): - Do something... + """Do something...""" - support = WebSupport(... - moderation_callback=moderation_callback) + support = WebSupport(..., moderation_callback=moderation_callback) The moderation callback must take one argument, which will be the same comment dict that is returned by add_comment. -- cgit v1.2.1 From 589e498b701bff65c4f7a895d99b4e2e16bb30a0 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 14 Aug 2010 16:05:19 -0500 Subject: simplify websupport writer now that we don't have nested commentable nodes. --- sphinx/writers/websupport.py | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index fbd3c1ef..c3d989b7 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -20,38 +20,22 @@ class WebSupportTranslator(HTMLTranslator): def __init__(self, builder, *args, **kwargs): HTMLTranslator.__init__(self, builder, *args, **kwargs) self.comment_class = 'spxcmt' - self.init_support() - - def init_support(self): - self.cur_node = None def dispatch_visit(self, node): if is_commentable(node): self.handle_visit_commentable(node) HTMLTranslator.dispatch_visit(self, node) - def dispatch_departure(self, node): - HTMLTranslator.dispatch_departure(self, node) - if is_commentable(node): - self.handle_depart_commentable(node) - def handle_visit_commentable(self, node): - # If this node is nested inside another commentable node this - # node will not be commented. - if self.cur_node is None: - self.cur_node = self.add_db_node(node) - # We will place the node in the HTML id attribute. If the node - # already has an id (for indexing purposes) put an empty - # span with the existing id directly before this node's HTML. - if node.attributes['ids']: - self.body.append('<span id="%s"></span>' - % node.attributes['ids'][0]) - node.attributes['ids'] = ['s%s' % self.cur_node.id] - node.attributes['classes'].append(self.comment_class) - - def handle_depart_commentable(self, node): - if self.comment_class in node.attributes['classes']: - self.cur_node = None + db_node = self.add_db_node(node) + # We will place the node in the HTML id attribute. If the node + # already has an id (for indexing purposes) put an empty + # span with the existing id directly before this node's HTML. + if node.attributes['ids']: + self.body.append('<span id="%s"></span>' + % node.attributes['ids'][0]) + node.attributes['ids'] = ['s%s' % db_node.id] + node.attributes['classes'].append(self.comment_class) def add_db_node(self, node): storage = self.builder.app.storage -- cgit v1.2.1 From 529ab7a1db4b0b47c6d0c2f6fe0bd7d4badf9cab Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 14 Aug 2010 17:43:20 -0500 Subject: resave pickle after uids are added --- sphinx/builders/websupport.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index ac9dd715..59bfd7c8 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -61,6 +61,23 @@ class WebSupportBuilder(StandaloneHTMLBuilder): stream=WarningStream(self.env._warnfunc)) return doctree + def resave_doctree(self, docname, doctree): + # make it picklable, save the reporter, it's needed later. + reporter = doctree.reporter + doctree.reporter = None + doctree.settings.warning_stream = None + doctree.settings.env = None + doctree.settings.record_dependencies = None + + fp = self.env.doc2path(docname, self.doctreedir, '.doctree') + f = open(fp, 'wb') + try: + pickle.dump(doctree, f, pickle.HIGHEST_PROTOCOL) + finally: + f.close() + + doctree.reporter = reporter + def write_doc(self, docname, doctree): destination = StringOutput(encoding='utf-8') doctree.settings = self.docsettings @@ -70,6 +87,7 @@ class WebSupportBuilder(StandaloneHTMLBuilder): list(merge_doctrees(old_doctree, doctree, is_commentable)) else: list(add_uids(doctree, is_commentable)) + self.resave_doctree(docname, doctree) self.cur_docname = docname self.secnumbers = self.env.toc_secnumbers.get(docname, {}) -- cgit v1.2.1 From 40bed63e8f9945d63c558b8d8ae259444c1c419d Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 14 Aug 2010 17:59:31 -0500 Subject: remove line column from node table --- sphinx/websupport/storage/__init__.py | 6 +----- sphinx/websupport/storage/db.py | 4 +--- sphinx/websupport/storage/sqlalchemystorage.py | 4 ++-- sphinx/writers/websupport.py | 1 - 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/sphinx/websupport/storage/__init__.py b/sphinx/websupport/storage/__init__.py index da815d0a..6a5ff49b 100644 --- a/sphinx/websupport/storage/__init__.py +++ b/sphinx/websupport/storage/__init__.py @@ -16,15 +16,11 @@ class StorageBackend(object): """ pass - def add_node(self, id, document, line, source): + def add_node(self, id, document, source): """Add a node to the StorageBackend. :param id: a unique id for the comment. - :param document: the name of the document the node belongs to. - - :param line: the line in the source where the node begins. - :param source: the source files name. """ raise NotImplementedError() diff --git a/sphinx/websupport/storage/db.py b/sphinx/websupport/storage/db.py index 54b16f22..be81a333 100644 --- a/sphinx/websupport/storage/db.py +++ b/sphinx/websupport/storage/db.py @@ -31,7 +31,6 @@ class Node(Base): id = Column(String(32), primary_key=True) document = Column(String(256), nullable=False) - line = Column(Integer) source = Column(Text, nullable=False) def nested_comments(self, username, moderator): @@ -94,10 +93,9 @@ class Node(Base): return comments - def __init__(self, id, document, line, source): + def __init__(self, id, document, source): self.id = id self.document = document - self.line = line self.source = source class Comment(Base): diff --git a/sphinx/websupport/storage/sqlalchemystorage.py b/sphinx/websupport/storage/sqlalchemystorage.py index d1683f60..174bef6e 100644 --- a/sphinx/websupport/storage/sqlalchemystorage.py +++ b/sphinx/websupport/storage/sqlalchemystorage.py @@ -33,8 +33,8 @@ class SQLAlchemyStorage(StorageBackend): def pre_build(self): self.build_session = Session() - def add_node(self, id, document, line, source): - node = Node(id, document, line, source) + def add_node(self, id, document, source): + node = Node(id, document, source) self.build_session.add(node) self.build_session.flush() return node diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index c3d989b7..6beb4b98 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -41,6 +41,5 @@ class WebSupportTranslator(HTMLTranslator): storage = self.builder.app.storage db_node_id = storage.add_node(id=node.uid, document=self.builder.cur_docname, - line=node.line, source=node.rawsource or node.astext()) return db_node_id -- cgit v1.2.1 From 52db0ccc1f27cafdaa2d2fea54fb9523293fadc6 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 14 Aug 2010 18:33:07 -0500 Subject: only add node if it doesn't already exist --- doc/web/quickstart.rst | 2 ++ sphinx/websupport/storage/__init__.py | 7 +++++++ sphinx/websupport/storage/sqlalchemystorage.py | 7 ++++++- sphinx/writers/websupport.py | 12 ++++++------ 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/doc/web/quickstart.rst b/doc/web/quickstart.rst index 61a432a8..fa93c6d9 100644 --- a/doc/web/quickstart.rst +++ b/doc/web/quickstart.rst @@ -6,6 +6,8 @@ Web Support Quick Start Building Documentation Data ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +New Test p + To make use of the web support package in your application you'll need to build the data it uses. This data includes pickle files representing documents, search indices, and node data that is used to track where diff --git a/sphinx/websupport/storage/__init__.py b/sphinx/websupport/storage/__init__.py index 6a5ff49b..3d8a9ab5 100644 --- a/sphinx/websupport/storage/__init__.py +++ b/sphinx/websupport/storage/__init__.py @@ -16,6 +16,13 @@ class StorageBackend(object): """ pass + def has_node(self, id): + """Check to see if a node exists. + + :param id: the id to check for. + """ + raise NotImplementedError() + def add_node(self, id, document, source): """Add a node to the StorageBackend. diff --git a/sphinx/websupport/storage/sqlalchemystorage.py b/sphinx/websupport/storage/sqlalchemystorage.py index 174bef6e..c775f3bb 100644 --- a/sphinx/websupport/storage/sqlalchemystorage.py +++ b/sphinx/websupport/storage/sqlalchemystorage.py @@ -33,11 +33,16 @@ class SQLAlchemyStorage(StorageBackend): def pre_build(self): self.build_session = Session() + def has_node(self, id): + session = Session() + node = session.query(Node).filter(Node.id == id).first() + session.close() + return True if node else False + def add_node(self, id, document, source): node = Node(id, document, source) self.build_session.add(node) self.build_session.flush() - return node def post_build(self): self.build_session.commit() diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index 6beb4b98..30e8c4dc 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -27,19 +27,19 @@ class WebSupportTranslator(HTMLTranslator): HTMLTranslator.dispatch_visit(self, node) def handle_visit_commentable(self, node): - db_node = self.add_db_node(node) # We will place the node in the HTML id attribute. If the node # already has an id (for indexing purposes) put an empty # span with the existing id directly before this node's HTML. + self.add_db_node(node) if node.attributes['ids']: self.body.append('<span id="%s"></span>' % node.attributes['ids'][0]) - node.attributes['ids'] = ['s%s' % db_node.id] + node.attributes['ids'] = ['s%s' % node.uid] node.attributes['classes'].append(self.comment_class) def add_db_node(self, node): storage = self.builder.app.storage - db_node_id = storage.add_node(id=node.uid, - document=self.builder.cur_docname, - source=node.rawsource or node.astext()) - return db_node_id + if not storage.has_node(node.uid): + storage.add_node(id=node.uid, + document=self.builder.cur_docname, + source=node.rawsource or node.astext()) -- cgit v1.2.1 From b7b6d165a23b9d61f372cdd2fcfea8d84f94f142 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 14 Aug 2010 18:34:01 -0500 Subject: remove the bogus paragraph I added to docs to test doctree merging --- doc/web/quickstart.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/web/quickstart.rst b/doc/web/quickstart.rst index fa93c6d9..61a432a8 100644 --- a/doc/web/quickstart.rst +++ b/doc/web/quickstart.rst @@ -6,8 +6,6 @@ Web Support Quick Start Building Documentation Data ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -New Test p - To make use of the web support package in your application you'll need to build the data it uses. This data includes pickle files representing documents, search indices, and node data that is used to track where -- cgit v1.2.1 From deb6ca579fb653ab49766ed261acc55df5e66584 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sat, 14 Aug 2010 20:13:55 -0500 Subject: remove old static dirs if they exist already --- sphinx/builders/websupport.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 59bfd7c8..283cff4f 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -173,13 +173,13 @@ class WebSupportBuilder(StandaloneHTMLBuilder): StandaloneHTMLBuilder.handle_finish(self) directories = ['_images', '_static'] for directory in directories: - try: - shutil.move(path.join(self.outdir, directory), - path.join(self.app.builddir, self.app.staticdir, - directory)) - except IOError: - # in case any of these directories don't exist - pass + src = path.join(self.outdir, directory) + dst = path.join(self.app.builddir, self.app.staticdir, directory) + if path.isdir(src): + if path.isdir(dst): + shutil.rmtree(dst) + shutil.move(src, dst) + for root, dirs, files in os.walk(self.doctreedir): for fn in files: fp = path.join(root, fn) -- cgit v1.2.1 From 3b316e8027df79eeb7c9aa9d146fb91979049e2f Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 11:56:42 +0200 Subject: Fix copyright info --- sphinx/builders/intl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index de147c82..abb119a4 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -5,7 +5,7 @@ The MessageCatalogBuilder class. - :copyright: Copyright 2010 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -- cgit v1.2.1 From 0047f9e1d2b3790741b7c2b04ddbc71cc78702ea Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 11:59:01 +0200 Subject: Fix line length --- sphinx/builders/intl.py | 3 ++- tests/test_build_gettext.py | 12 ++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index abb119a4..7b01602e 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -88,7 +88,8 @@ class MessageCatalogBuilder(Builder): pofile.write(POHEADER % data) for message in messages: # message contains *one* line of text ready for translation - message = message.replace(u'\\', ur'\\').replace(u'"', ur'\"') + message = message.replace(u'\\', ur'\\'). \ + replace(u'"', ur'\"') pomsg = u'msgid "%s"\nmsgstr ""\n\n' % message pofile.write(pomsg.encode('utf-8')) finally: diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index 9e98c36d..3312f4f2 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -46,7 +46,8 @@ def test_gettext(app): if p.returncode != 0: print stdout print stderr - assert False, 'msginit exited with return code %s' % p.returncode + assert False, 'msginit exited with return code %s' % \ + p.returncode assert (app.outdir / 'en_US.po').isfile(), 'msginit failed' try: p = Popen(['msgfmt', 'en_US.po', '-o', @@ -59,8 +60,10 @@ def test_gettext(app): if p.returncode != 0: print stdout print stderr - assert False, 'msgfmt exited with return code %s' % p.returncode - assert (app.outdir / 'en' / 'LC_MESSAGES' / 'test_root.mo').isfile(), 'msgfmt failed' + assert False, 'msgfmt exited with return code %s' % \ + p.returncode + assert (app.outdir / 'en' / 'LC_MESSAGES' / 'test_root.mo').isfile(), \ + 'msgfmt failed' finally: os.chdir(cwd) @@ -95,7 +98,8 @@ def setup_patch(): print stdout print stderr assert False, 'msgfmt exited with return code %s' % p.returncode - assert (test_root / 'xx' / 'LC_MESSAGES' / 'bom.mo').isfile(), 'msgfmt failed' + assert (test_root / 'xx' / 'LC_MESSAGES' / 'bom.mo').isfile(), \ + 'msgfmt failed' def teardown_patch(): (test_root / 'xx').rmtree() test_patch.setup = setup_patch -- cgit v1.2.1 From e0d22ea2a2d50fa24c4a6b9ece44282de5c20703 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 12:04:27 +0200 Subject: Added a newline for readability --- tests/test_build_gettext.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index 3312f4f2..772bba87 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -100,6 +100,7 @@ def setup_patch(): assert False, 'msgfmt exited with return code %s' % p.returncode assert (test_root / 'xx' / 'LC_MESSAGES' / 'bom.mo').isfile(), \ 'msgfmt failed' + def teardown_patch(): (test_root / 'xx').rmtree() test_patch.setup = setup_patch -- cgit v1.2.1 From 96d931d573fc58cd391f77073b1a592e27b71cb2 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 12:13:04 +0200 Subject: Move i18n part of the MessageCatalogBuilder in a seperate one --- sphinx/builders/intl.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 7b01602e..fa1dc82a 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -9,7 +9,7 @@ :license: BSD, see LICENSE for details. """ -import collections +from collections import defaultdict from datetime import datetime from os import path @@ -41,14 +41,12 @@ msgstr "" """[1:] -class MessageCatalogBuilder(Builder): - """ - Builds gettext-style message catalogs (.pot files). - """ - name = 'gettext' +class I18NBuilder(Builder): + name = 'i18n' def init(self): - self.catalogs = collections.defaultdict(list) + Builder.init(self) + self.catalogs = defaultdict(list) def get_target_uri(self, docname, typ=None): return '' @@ -60,17 +58,17 @@ class MessageCatalogBuilder(Builder): return def write_doc(self, docname, doctree): - """ - Store a document's translatable strings in the message catalog of its - section. For this purpose a document's *top-level directory* -- or - otherwise its *name* -- is considered its section. - """ catalog = self.catalogs[docname.split(SEP, 1)[0]] for _, msg in extract_messages(doctree): - # XXX msgctxt for duplicate messages if msg not in catalog: catalog.append(msg) +class MessageCatalogBuilder(I18NBuilder): + """ + Builds gettext-style message catalogs (.pot files). + """ + name = 'gettext' + def finish(self): data = dict( version = self.config.version, -- cgit v1.2.1 From 182585a515d39057efc237a1029aeb73797bd7cc Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 12:16:48 +0200 Subject: Fix test which was broken to change in the path object api --- tests/test_build_gettext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index 772bba87..6a770869 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -78,7 +78,7 @@ def test_all(app): confoverrides={'language': 'xx', 'locale_dirs': ['.']}) def test_patch(app): app.builder.build(['bom']) - result = (app.outdir / 'bom.txt').text('utf-8') + result = (app.outdir / 'bom.txt').text(encoding='utf-8') expect = (u"\nDatei mit UTF-8" u"\n***************\n" # underline matches new translation u"\nThis file has umlauts: äöü.\n") -- cgit v1.2.1 From b77ad9dc3addc701716a93444850582dfa942dae Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 12:22:43 +0200 Subject: Monkey patch .gettext with .ugettext if possible (we use python 2.x) --- sphinx/environment.py | 2 +- sphinx/locale/__init__.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/sphinx/environment.py b/sphinx/environment.py index 6339675b..4809158d 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -209,7 +209,7 @@ class Locale(Transform): for node, msg in extract_messages(self.document): ctx = node.parent patch = new_document(source, settings) - msgstr = catalog.ugettext(msg) + msgstr = catalog.gettext(msg) #XXX add marker to untranslated parts if not msgstr or msgstr == msg: # as-of-yet untranslated continue diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py index 48116991..2d3ab026 100644 --- a/sphinx/locale/__init__.py +++ b/sphinx/locale/__init__.py @@ -217,4 +217,6 @@ def init(locale_dirs, language, catalog='sphinx'): translator = gettext.NullTranslations() has_translation = False translators[catalog] = translator + if hasattr(translator, 'ugettext'): + translator.gettext = translator.ugettext return translator, has_translation -- cgit v1.2.1 From 4a9b05855dad0cbebc40f44fba1ff86225d159e3 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 12:26:37 +0200 Subject: Use codecs.open with python 2.x in the MessageCatalogBuilder --- sphinx/builders/intl.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index fa1dc82a..d4f5d837 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -12,6 +12,7 @@ from collections import defaultdict from datetime import datetime from os import path +from codecs import open from docutils import nodes @@ -81,7 +82,8 @@ class MessageCatalogBuilder(I18NBuilder): self.catalogs.iteritems(), "writing message catalogs... ", lambda (section, _):darkgreen(section), len(self.catalogs)): - pofile = open(path.join(self.outdir, '%s.pot' % section), 'w') + pofp = path.join(self.outdir, section + '.pot') + pofile = open(pofp, 'w', encoding='utf-8') try: pofile.write(POHEADER % data) for message in messages: @@ -89,6 +91,6 @@ class MessageCatalogBuilder(I18NBuilder): message = message.replace(u'\\', ur'\\'). \ replace(u'"', ur'\"') pomsg = u'msgid "%s"\nmsgstr ""\n\n' % message - pofile.write(pomsg.encode('utf-8')) + pofile.write(pomsg) finally: pofile.close() -- cgit v1.2.1 From e7b7b4d0767d3d45a34a5a386e294a3c52e7086d Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 12:30:13 +0200 Subject: Fix test_gettext test for python 3.x --- tests/test_build_gettext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index 6a770869..581c1cb8 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -67,7 +67,7 @@ def test_gettext(app): finally: os.chdir(cwd) - _ = gettext.translation('test_root', app.outdir, languages=['en']).ugettext + _ = gettext.translation('test_root', app.outdir, languages=['en']).gettext assert _("Testing various markup") == u"Testing various markup" @with_app(buildername='gettext') -- cgit v1.2.1 From 2e3b2922b217b9be6f81b2984d2cbaf0e14e06d7 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 12:53:00 +0200 Subject: Added versioning support to i18n builder --- sphinx/builders/intl.py | 68 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index d4f5d837..88c128f5 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -13,13 +13,18 @@ from collections import defaultdict from datetime import datetime from os import path from codecs import open +import os +import pickle from docutils import nodes +from docutils.utils import Reporter from sphinx.builders import Builder from sphinx.util.nodes import extract_messages -from sphinx.util.osutil import SEP +from sphinx.util.osutil import SEP, copyfile from sphinx.util.console import darkgreen +from sphinx.environment import WarningStream +from sphinx.versioning import add_uids, merge_doctrees POHEADER = ur""" # SOME DESCRIPTIVE TITLE. @@ -47,7 +52,42 @@ class I18NBuilder(Builder): def init(self): Builder.init(self) - self.catalogs = defaultdict(list) + self.catalogs = defaultdict(dict) + for root, dirs, files in os.walk(self.doctreedir): + for fn in files: + fp = path.join(root, fn) + if fp.endswith('.doctree'): + copyfile(fp, fp + '.old') + + def get_old_doctree(self, docname): + fp = self.env.doc2path(docname, self.doctreedir, '.doctree.old') + try: + f = open(fp, 'rb') + try: + doctree = pickle.load(f) + finally: + f.close() + except IOError: + return None + doctree.settings.env = self.env + doctree.reporter = Reporter(self.env.doc2path(docname), 2, 5, + stream=WarningStream(self.env._warnfunc)) + + def resave_doctree(self, docname, doctree): + reporter = doctree.reporter + doctree.reporter = None + doctree.settings.warning_stream = None + doctree.settings.env = None + doctree.settings.record_dependencies = None + + fp = self.env.doc2path(docname, self.doctreedir, '.doctree') + f = open(fp, 'wb') + try: + pickle.dump(doctree, f, pickle.HIGHEST_PROTOCOL) + finally: + f.close() + + doctree.reporter = reporter def get_target_uri(self, docname, typ=None): return '' @@ -60,9 +100,24 @@ class I18NBuilder(Builder): def write_doc(self, docname, doctree): catalog = self.catalogs[docname.split(SEP, 1)[0]] - for _, msg in extract_messages(doctree): - if msg not in catalog: - catalog.append(msg) + old_doctree = self.get_old_doctree(docname) + + if old_doctree: + list(merge_doctrees(old_doctree, doctree, nodes.TextElement)) + else: + list(add_uids(doctree, nodes.TextElement)) + self.resave_doctree(docname, doctree) + + for node, msg in extract_messages(doctree): + catalog.setdefault(node.uid, msg) + + def finish(self): + Builder.finish(self) + for root, dirs, files in os.walk(self.doctreedir): + for fn in files: + fp = path.join(root, fn) + if fp.endswith('.doctree.old'): + os.remove(fp) class MessageCatalogBuilder(I18NBuilder): """ @@ -71,6 +126,7 @@ class MessageCatalogBuilder(I18NBuilder): name = 'gettext' def finish(self): + I18NBuilder.finish(self) data = dict( version = self.config.version, copyright = self.config.copyright, @@ -86,7 +142,7 @@ class MessageCatalogBuilder(I18NBuilder): pofile = open(pofp, 'w', encoding='utf-8') try: pofile.write(POHEADER % data) - for message in messages: + for message in messages.itervalues(): # message contains *one* line of text ready for translation message = message.replace(u'\\', ur'\\'). \ replace(u'"', ur'\"') -- cgit v1.2.1 From 8702e0c4fdd07975f56c9c9b190c95e8da0197e0 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 13:18:47 +0200 Subject: Before each id, str pair a comment with the uid can be found in the pot files --- sphinx/builders/intl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 88c128f5..19190926 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -142,11 +142,11 @@ class MessageCatalogBuilder(I18NBuilder): pofile = open(pofp, 'w', encoding='utf-8') try: pofile.write(POHEADER % data) - for message in messages.itervalues(): + for uid, message in messages.iteritems(): # message contains *one* line of text ready for translation message = message.replace(u'\\', ur'\\'). \ replace(u'"', ur'\"') - pomsg = u'msgid "%s"\nmsgstr ""\n\n' % message + pomsg = u'#%s\nmsgid "%s"\nmsgstr ""\n\n' % (uid, message) pofile.write(pomsg) finally: pofile.close() -- cgit v1.2.1 From ed96be59816ec90d24a0a99396101672bb51e979 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 13:38:10 +0200 Subject: Added a VersioningBuilderMixin --- sphinx/builders/versioning.py | 71 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 sphinx/builders/versioning.py diff --git a/sphinx/builders/versioning.py b/sphinx/builders/versioning.py new file mode 100644 index 00000000..f00e08c2 --- /dev/null +++ b/sphinx/builders/versioning.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +""" + sphinx.builders.versioning + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" +import os +import pickle + +from docutils.utils import Reporter + +from sphinx.util.osutil import copyfile +from sphinx.environment import WarningStream +from sphinx.versioning import add_uids, merge_doctrees + + +class VersioningBuilderMixin(object): + def walk_doctree_files(self): + for root, dirs, files in os.walk(self.doctreedir): + for fn in files: + yield os.path.join(root, fn) + + def init(self): + for fp in self.walk_doctree_files(self): + if fp.endswith('.doctree'): + copyfile(fp, fp + '.old') + + def get_old_doctree(self, docname): + fp = self.env.doc2path(docname, self.doctreedir, '.doctree.old') + try: + f = open(fp, 'rb') + try: + doctree = pickle.load(f) + finally: + f.close() + except IOError: + return None + doctree.settings.env = self.env + doctree.reporter = Reporter(self.env.doc2path(docname), 2, 5, + stream=WarningStream(self.env._warnfunc)) + + def resave_doctree(self, docname, doctree): + reporter = doctree.reporter + doctree.reporter = None + doctree.settings.warning_stream = None + doctree.settings.env = None + doctree.settings.record_dependencies = None + + fp = self.env.doc2path(docname, self.doctreedir, '.doctree') + f = open(fp, 'wb') + try: + pickle.dump(doctree, f, pickle.HIGHEST_PROTOCOL) + finally: + f.close() + + doctree.reporter = reporter + + def handle_versioning(self, docname, doctree, condition): + old_doctree = self.get_old_doctree(docname) + if old_doctree: + list(merge_doctrees(old_doctree, doctree, condition)) + else: + list(add_uids(doctree, condition)) + self.resave_doctree(docname, doctree) + + def finish(self): + for fp in self.walk_doctree_files(self): + if fp.endswith('.doctree.old'): + os.remove(fp) -- cgit v1.2.1 From 70a810961b3f466ae7920c816c13d78056ec6cd3 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 13:44:14 +0200 Subject: Fix an error --- sphinx/builders/versioning.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/builders/versioning.py b/sphinx/builders/versioning.py index f00e08c2..c47dcc98 100644 --- a/sphinx/builders/versioning.py +++ b/sphinx/builders/versioning.py @@ -23,7 +23,7 @@ class VersioningBuilderMixin(object): yield os.path.join(root, fn) def init(self): - for fp in self.walk_doctree_files(self): + for fp in self.walk_doctree_files(): if fp.endswith('.doctree'): copyfile(fp, fp + '.old') @@ -66,6 +66,6 @@ class VersioningBuilderMixin(object): self.resave_doctree(docname, doctree) def finish(self): - for fp in self.walk_doctree_files(self): + for fp in self.walk_doctree_files(): if fp.endswith('.doctree.old'): os.remove(fp) -- cgit v1.2.1 From d212117cd3b91d5aead223e619b438db817ab4a6 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 13:48:38 +0200 Subject: Switch VersioningBuilderMixin --- sphinx/builders/intl.py | 57 +++++-------------------------------------------- 1 file changed, 5 insertions(+), 52 deletions(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 19190926..0af5b19a 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -13,18 +13,14 @@ from collections import defaultdict from datetime import datetime from os import path from codecs import open -import os -import pickle from docutils import nodes -from docutils.utils import Reporter from sphinx.builders import Builder +from sphinx.builders.versioning import VersioningBuilderMixin from sphinx.util.nodes import extract_messages from sphinx.util.osutil import SEP, copyfile from sphinx.util.console import darkgreen -from sphinx.environment import WarningStream -from sphinx.versioning import add_uids, merge_doctrees POHEADER = ur""" # SOME DESCRIPTIVE TITLE. @@ -47,47 +43,13 @@ msgstr "" """[1:] -class I18NBuilder(Builder): +class I18NBuilder(Builder, VersioningBuilderMixin): name = 'i18n' def init(self): Builder.init(self) + VersioningBuilderMixin.init(self) self.catalogs = defaultdict(dict) - for root, dirs, files in os.walk(self.doctreedir): - for fn in files: - fp = path.join(root, fn) - if fp.endswith('.doctree'): - copyfile(fp, fp + '.old') - - def get_old_doctree(self, docname): - fp = self.env.doc2path(docname, self.doctreedir, '.doctree.old') - try: - f = open(fp, 'rb') - try: - doctree = pickle.load(f) - finally: - f.close() - except IOError: - return None - doctree.settings.env = self.env - doctree.reporter = Reporter(self.env.doc2path(docname), 2, 5, - stream=WarningStream(self.env._warnfunc)) - - def resave_doctree(self, docname, doctree): - reporter = doctree.reporter - doctree.reporter = None - doctree.settings.warning_stream = None - doctree.settings.env = None - doctree.settings.record_dependencies = None - - fp = self.env.doc2path(docname, self.doctreedir, '.doctree') - f = open(fp, 'wb') - try: - pickle.dump(doctree, f, pickle.HIGHEST_PROTOCOL) - finally: - f.close() - - doctree.reporter = reporter def get_target_uri(self, docname, typ=None): return '' @@ -100,24 +62,15 @@ class I18NBuilder(Builder): def write_doc(self, docname, doctree): catalog = self.catalogs[docname.split(SEP, 1)[0]] - old_doctree = self.get_old_doctree(docname) - if old_doctree: - list(merge_doctrees(old_doctree, doctree, nodes.TextElement)) - else: - list(add_uids(doctree, nodes.TextElement)) - self.resave_doctree(docname, doctree) + self.handle_versioning(docname, doctree, nodes.TextElement) for node, msg in extract_messages(doctree): catalog.setdefault(node.uid, msg) def finish(self): Builder.finish(self) - for root, dirs, files in os.walk(self.doctreedir): - for fn in files: - fp = path.join(root, fn) - if fp.endswith('.doctree.old'): - os.remove(fp) + VersioningBuilderMixin.finish(self) class MessageCatalogBuilder(I18NBuilder): """ -- cgit v1.2.1 From 3a3a46c50b142c16140dfac04bf947732398377f Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 13:56:01 +0200 Subject: Switch to VersioningBuilderMixin --- sphinx/builders/websupport.py | 59 ++++--------------------------------------- 1 file changed, 5 insertions(+), 54 deletions(-) diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 283cff4f..303adfe6 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -12,23 +12,20 @@ import cPickle as pickle from os import path from cgi import escape -import os import posixpath import shutil from docutils.io import StringOutput -from docutils.utils import Reporter from sphinx.util.osutil import os_path, relative_uri, ensuredir, copyfile from sphinx.util.jsonimpl import dumps as dump_json from sphinx.util.websupport import is_commentable from sphinx.builders.html import StandaloneHTMLBuilder +from sphinx.builders.versioning import VersioningBuilderMixin from sphinx.writers.websupport import WebSupportTranslator -from sphinx.environment import WarningStream -from sphinx.versioning import add_uids, merge_doctrees -class WebSupportBuilder(StandaloneHTMLBuilder): +class WebSupportBuilder(StandaloneHTMLBuilder, VersioningBuilderMixin): """ Builds documents for the web support package. """ @@ -37,57 +34,16 @@ class WebSupportBuilder(StandaloneHTMLBuilder): def init(self): StandaloneHTMLBuilder.init(self) - for root, dirs, files in os.walk(self.doctreedir): - for fn in files: - fp = path.join(root, fn) - if fp.endswith('.doctree'): - copyfile(fp, fp + '.old') + VersioningBuilderMixin.init(self) def init_translator_class(self): self.translator_class = WebSupportTranslator - def get_old_doctree(self, docname): - fp = self.env.doc2path(docname, self.doctreedir, '.doctree.old') - try: - f = open(fp, 'rb') - try: - doctree = pickle.load(f) - finally: - f.close() - except IOError: - return None - doctree.settings.env = self.env - doctree.reporter = Reporter(self.env.doc2path(docname), 2, 5, - stream=WarningStream(self.env._warnfunc)) - return doctree - - def resave_doctree(self, docname, doctree): - # make it picklable, save the reporter, it's needed later. - reporter = doctree.reporter - doctree.reporter = None - doctree.settings.warning_stream = None - doctree.settings.env = None - doctree.settings.record_dependencies = None - - fp = self.env.doc2path(docname, self.doctreedir, '.doctree') - f = open(fp, 'wb') - try: - pickle.dump(doctree, f, pickle.HIGHEST_PROTOCOL) - finally: - f.close() - - doctree.reporter = reporter - def write_doc(self, docname, doctree): destination = StringOutput(encoding='utf-8') doctree.settings = self.docsettings - old_doctree = self.get_old_doctree(docname) - if old_doctree: - list(merge_doctrees(old_doctree, doctree, is_commentable)) - else: - list(add_uids(doctree, is_commentable)) - self.resave_doctree(docname, doctree) + self.handle_versioning(docname, doctree, is_commentable) self.cur_docname = docname self.secnumbers = self.env.toc_secnumbers.get(docname, {}) @@ -171,6 +127,7 @@ class WebSupportBuilder(StandaloneHTMLBuilder): def handle_finish(self): StandaloneHTMLBuilder.handle_finish(self) + VersioningBuilderMixin.finish(self) directories = ['_images', '_static'] for directory in directories: src = path.join(self.outdir, directory) @@ -180,12 +137,6 @@ class WebSupportBuilder(StandaloneHTMLBuilder): shutil.rmtree(dst) shutil.move(src, dst) - for root, dirs, files in os.walk(self.doctreedir): - for fn in files: - fp = path.join(root, fn) - if fp.endswith('.doctree.old'): - os.remove(fp) - def dump_search_index(self): self.indexer.finish_indexing() -- cgit v1.2.1 From 13f78a54ee25405eee56a67d0f9d952afc7135ad Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Sun, 15 Aug 2010 10:43:52 -0500 Subject: return doctree from get_old_doctree --- sphinx/builders/versioning.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sphinx/builders/versioning.py b/sphinx/builders/versioning.py index c47dcc98..6c2bccca 100644 --- a/sphinx/builders/versioning.py +++ b/sphinx/builders/versioning.py @@ -40,6 +40,7 @@ class VersioningBuilderMixin(object): doctree.settings.env = self.env doctree.reporter = Reporter(self.env.doc2path(docname), 2, 5, stream=WarningStream(self.env._warnfunc)) + return doctree def resave_doctree(self, docname, doctree): reporter = doctree.reporter -- cgit v1.2.1 From e20779803bba96609e7777b70f9b7fc73f4aca3c Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 18:08:00 +0200 Subject: Return doctree in get_old_doctree --- sphinx/builders/versioning.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sphinx/builders/versioning.py b/sphinx/builders/versioning.py index c47dcc98..6c2bccca 100644 --- a/sphinx/builders/versioning.py +++ b/sphinx/builders/versioning.py @@ -40,6 +40,7 @@ class VersioningBuilderMixin(object): doctree.settings.env = self.env doctree.reporter = Reporter(self.env.doc2path(docname), 2, 5, stream=WarningStream(self.env._warnfunc)) + return doctree def resave_doctree(self, docname, doctree): reporter = doctree.reporter -- cgit v1.2.1 From 21f628bc8dbc4046850334a93debfdb974e08e58 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 18:41:14 +0200 Subject: Added a test for paragraphs inserted at the beginning of a document --- tests/root/versioning/index.txt | 1 + tests/root/versioning/insert_beginning.txt | 18 ++++++++++++++++++ tests/test_versioning.py | 7 +++++++ 3 files changed, 26 insertions(+) create mode 100644 tests/root/versioning/insert_beginning.txt diff --git a/tests/root/versioning/index.txt b/tests/root/versioning/index.txt index 234e223f..a6d12007 100644 --- a/tests/root/versioning/index.txt +++ b/tests/root/versioning/index.txt @@ -9,3 +9,4 @@ Versioning Stuff deleted deleted_end modified + insert_beginning diff --git a/tests/root/versioning/insert_beginning.txt b/tests/root/versioning/insert_beginning.txt new file mode 100644 index 00000000..57102a76 --- /dev/null +++ b/tests/root/versioning/insert_beginning.txt @@ -0,0 +1,18 @@ +Versioning test text +==================== + +Apperantly inserting a paragraph at the beginning of a document caused +problems earlier so this document should be used to test that. + +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_versioning.py b/tests/test_versioning.py index 54a48f4a..549d760f 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -102,3 +102,10 @@ def test_insert(): assert len(new_nodes) == 1 assert original_uids[0] == uids[0] assert original_uids[1:] == uids[2:] + +def test_insert_beginning(): + insert_beginning = doctrees['versioning/insert_beginning'] + new_nodes = list(merge_doctrees(original, insert_beginning, is_paragraph)) + uids = [n.uid for n in insert_beginning.traverse(is_paragraph)] + assert len(new_nodes) == 1 + assert original_uids == uids[1:] -- cgit v1.2.1 From fe7f6bc0ba14b08c478d20aa9ef0d80ad11f05ca Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 18:44:09 +0200 Subject: Check also for the length and explicitly check that the first uids are different --- tests/test_versioning.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 549d760f..136faa1c 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -108,4 +108,6 @@ def test_insert_beginning(): new_nodes = list(merge_doctrees(original, insert_beginning, is_paragraph)) uids = [n.uid for n in insert_beginning.traverse(is_paragraph)] assert len(new_nodes) == 1 + assert len(uids) == 4 assert original_uids == uids[1:] + assert original_uids[0] != uids[0] -- cgit v1.2.1 From b0721f4dd2d20828624d07ab0d6a0a8a8980fa0e Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 19:25:46 +0200 Subject: Added a test for the behavior described by Jacob and it does fail --- tests/root/versioning/index.txt | 1 + tests/root/versioning/insert_similar.txt | 17 +++++++++++++++++ tests/test_versioning.py | 9 +++++++++ 3 files changed, 27 insertions(+) create mode 100644 tests/root/versioning/insert_similar.txt diff --git a/tests/root/versioning/index.txt b/tests/root/versioning/index.txt index a6d12007..9d098f75 100644 --- a/tests/root/versioning/index.txt +++ b/tests/root/versioning/index.txt @@ -10,3 +10,4 @@ Versioning Stuff deleted_end modified insert_beginning + insert_similar diff --git a/tests/root/versioning/insert_similar.txt b/tests/root/versioning/insert_similar.txt new file mode 100644 index 00000000..ee9b5305 --- /dev/null +++ b/tests/root/versioning/insert_similar.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. It's not really interesting, in fact it is *really* +boring. + +Anyway I need more + +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_versioning.py b/tests/test_versioning.py index 136faa1c..5c31f0c3 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -111,3 +111,12 @@ def test_insert_beginning(): assert len(uids) == 4 assert original_uids == uids[1:] assert original_uids[0] != uids[0] + +def test_insert_similar(): + insert_similar = doctrees['versioning/insert_similar'] + new_nodes = list(merge_doctrees(original, insert_similar, is_paragraph)) + uids = [n.uid for n in insert_similar.traverse(is_paragraph)] + assert len(new_nodes) == 1 + assert new_nodes[0].rawsource == u'Anyway I need more' + assert original_uids[0] == uids[0] + assert original_uids[1:] == uids[2:] -- cgit v1.2.1 From ef09641f0e85e73e0f38a8c4e4e3006678e978c8 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 20:34:08 +0200 Subject: Replaced the merging algorithm with one that handles similarities better, it's awfully slow though, if anybody has a better idea please implement it --- sphinx/versioning.py | 112 ++++++++++++--------------------------------------- 1 file changed, 26 insertions(+), 86 deletions(-) diff --git a/sphinx/versioning.py b/sphinx/versioning.py index d0ea18a7..0b2b1f24 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -10,12 +10,9 @@ :license: BSD, see LICENSE for details. """ from uuid import uuid4 +from operator import itemgetter +from collections import defaultdict from itertools import product -try: - from itertools import izip_longest as zip_longest -except ImportError: - from itertools import zip_longest -from difflib import SequenceMatcher from sphinx.util import PeekableIterator @@ -34,19 +31,6 @@ def add_uids(doctree, 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 @@ -58,78 +42,34 @@ def merge_doctrees(old, new, condition): :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 zip_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) + old_nodes = old.traverse(condition) + new_nodes = new.traverse(condition) + ratios = defaultdict(list) + for old_node, new_node in product(old_nodes, new_nodes): + ratios[old_node, new_node] = get_ratio(old_node.rawsource, + new_node.rawsource) + ratios = sorted(ratios.iteritems(), key=itemgetter(1)) + seen = set() + for (old_node, new_node), ratio in ratios: + if new_node in seen: 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): + else: + seen.add(new_node) + if ratio < 65: + new_node.uid = old_node.uid + else: + new_node.uid = uuid4().hex + yield new_node + +def get_ratio(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. + Returns a "similiarity ratio" representing the similarity between the two + strings where 0 is equal and anything above less than equal. """ 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 + return 0 + ratio = levenshtein_distance(old, new) / (len(old) / 100.0) + return ratio def levenshtein_distance(a, b): if len(a) < len(b): -- cgit v1.2.1 From baea95942d71053c4b723252ebcb57902f1ed469 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 20:34:58 +0200 Subject: shutil.copytree has no ignore argument in python 2.4 --- tests/path.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/path.py b/tests/path.py index df96bce4..d4bd7ddf 100644 --- a/tests/path.py +++ b/tests/path.py @@ -88,7 +88,7 @@ class path(str): """ shutil.rmtree(self, ignore_errors=ignore_errors, onerror=onerror) - def copytree(self, destination, symlinks=False, ignore=None): + def copytree(self, destination, symlinks=False): """ Recursively copy a directory to the given `destination`. If the given `destination` does not exist it will be created. @@ -102,7 +102,7 @@ class path(str): A callback which gets called with the path of the directory being copied and a list of paths as returned by :func:`os.listdir`. """ - shutil.copytree(self, destination, symlinks=symlinks, ignore=ignore) + shutil.copytree(self, destination, symlinks=symlinks) def movetree(self, destination): """ -- cgit v1.2.1 From 4649062475d1502a171cf16e139b79ffb4e89de0 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 20:48:19 +0200 Subject: Use uuid as a requirement in the setup.py and mention it in the documentation for Python 2.4 --- doc/intro.rst | 4 ++++ setup.py | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/doc/intro.rst b/doc/intro.rst index c85fbbad..caff141d 100644 --- a/doc/intro.rst +++ b/doc/intro.rst @@ -50,10 +50,14 @@ docutils_ and Jinja2_ libraries. Sphinx should work with docutils version 0.5 or some (not broken) SVN trunk snapshot. If you like to have source code highlighting support, you must also install the Pygments_ library. +If you use **Python 2.4** you also need uuid_. + .. _reStructuredText: http://docutils.sf.net/rst.html .. _docutils: http://docutils.sf.net/ .. _Jinja2: http://jinja.pocoo.org/2/ .. _Pygments: http://pygments.org/ +.. The given homepage is only a directory listing so I'm using the pypi site. +.. _uuid: http://pypi.python.org/pypi/uuid/ Usage diff --git a/setup.py b/setup.py index fe4066b8..af9e660c 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ A development egg can be found `here <http://bitbucket.org/birkenfeld/sphinx/get/tip.gz#egg=Sphinx-dev>`_. ''' -requires = ['Pygments>=0.8', 'Jinja2>=2.2', 'docutils>=0.5'] +requires = ['Pygments>=0.8', 'Jinja2>=2.2', 'docutils>=0.5', 'uuid>=1.30'] if sys.version_info < (2, 4): print('ERROR: Sphinx requires at least Python 2.4 to run.') @@ -61,7 +61,10 @@ if sys.version_info < (2, 5): except: pass else: - del requires[-1] + del requires[-2] +elif sys.version_info >= (2, 5): + # An uuid module has been added to the stdlib in 2.5 + del requires[-1] # Provide a "compile_catalog" command that also creates the translated -- cgit v1.2.1 From 209553e565d38503bc0264682ad13ed8435acf52 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 21:06:47 +0200 Subject: Make levenshtein implementation faster for equal strings --- sphinx/versioning.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sphinx/versioning.py b/sphinx/versioning.py index 0b2b1f24..75362904 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -66,12 +66,11 @@ def get_ratio(old, new): Returns a "similiarity ratio" representing the similarity between the two strings where 0 is equal and anything above less than equal. """ - if old == new: - return 0 - ratio = levenshtein_distance(old, new) / (len(old) / 100.0) - return ratio + return levenshtein_distance(old, new) / (len(old) / 100.0) def levenshtein_distance(a, b): + if a == b: + return 0 if len(a) < len(b): a, b = b, a if not a: -- cgit v1.2.1 From 3542ae7553f8fb767de577cf0456291da8ac84e9 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sun, 15 Aug 2010 21:25:31 +0200 Subject: Optimized merging algorithm --- sphinx/versioning.py | 12 +++++++++--- tests/test_versioning.py | 14 +------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/sphinx/versioning.py b/sphinx/versioning.py index 75362904..5f325455 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -45,11 +45,17 @@ def merge_doctrees(old, new, condition): old_nodes = old.traverse(condition) new_nodes = new.traverse(condition) ratios = defaultdict(list) + seen = set() for old_node, new_node in product(old_nodes, new_nodes): - ratios[old_node, new_node] = get_ratio(old_node.rawsource, - new_node.rawsource) + if new_node in seen: + continue + ratio = get_ratio(old_node.rawsource, new_node.rawsource) + if ratio == 0: + new_node.uid = old_node.uid + seen.add(new_node) + else: + ratios[old_node, new_node] = ratio ratios = sorted(ratios.iteritems(), key=itemgetter(1)) - seen = set() for (old_node, new_node), ratio in ratios: if new_node in seen: continue diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 5c31f0c3..06c4ff2f 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -16,7 +16,7 @@ from docutils.statemachine import ViewList from docutils.parsers.rst.directives.html import MetaBody from sphinx import addnodes -from sphinx.versioning import make_diff, add_uids, merge_doctrees +from sphinx.versioning import add_uids, merge_doctrees def setup_module(): global app, original, original_uids @@ -36,18 +36,6 @@ 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' -- cgit v1.2.1 From 7707a73959b896eb496d4d45cb6431c35a6e2bec Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Mon, 16 Aug 2010 08:10:14 +0200 Subject: Optimized further and added several comments explaining the merging algorithm --- sphinx/versioning.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/sphinx/versioning.py b/sphinx/versioning.py index 5f325455..9a7cb2da 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -13,6 +13,10 @@ from uuid import uuid4 from operator import itemgetter from collections import defaultdict from itertools import product +try: + from itertools import izip_longest as zip_longest +except ImportError: + from itertools import zip_longest from sphinx.util import PeekableIterator @@ -42,12 +46,32 @@ def merge_doctrees(old, new, condition): :param condition: A callable which returns either ``True`` or ``False`` for a given node. """ - old_nodes = old.traverse(condition) - new_nodes = new.traverse(condition) + old_iter = old.traverse(condition) + new_iter = new.traverse(condition) + old_nodes = [] + new_nodes = [] ratios = defaultdict(list) seen = set() + # compare the nodes each doctree in order + for old_node, new_node in zip_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 + ratio = get_ratio(old_node.rawsource, new_node.rawsource) + if ratio == 0: + new_node.uid = old_node.uid + seen.add(new_node) + else: + ratios[old_node, new_node] = ratio + old_nodes.append(old_node) + new_nodes.append(new_node) + # calculate the ratios for each unequal pair of nodes, should we stumble + # on a pair which is equal we set the uid and add it to the seen ones for old_node, new_node in product(old_nodes, new_nodes): - if new_node in seen: + if new_node in seen or (old_node, new_node) in ratios: continue ratio = get_ratio(old_node.rawsource, new_node.rawsource) if ratio == 0: @@ -55,6 +79,9 @@ def merge_doctrees(old, new, condition): seen.add(new_node) else: ratios[old_node, new_node] = ratio + # choose the old node with the best ratio for each new node and set the uid + # as long as the ratio is under a certain value, in which case we consider + # them not changed but different ratios = sorted(ratios.iteritems(), key=itemgetter(1)) for (old_node, new_node), ratio in ratios: if new_node in seen: @@ -66,6 +93,11 @@ def merge_doctrees(old, new, condition): else: new_node.uid = uuid4().hex yield new_node + # create new uuids for any new node we left out earlier, this happens + # if one or more nodes are simply added. + for new_node in set(new_nodes) - seen: + new_node.uid = uuid4().hex + yield new_node def get_ratio(old, new): """ -- cgit v1.2.1 From cf29a28446dd017193f509c8ca6403fc25995164 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Mon, 16 Aug 2010 08:18:10 +0200 Subject: Fix get_ratio for empty strings --- sphinx/versioning.py | 7 ++++++- tests/test_versioning.py | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/sphinx/versioning.py b/sphinx/versioning.py index 9a7cb2da..06acc63d 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -20,6 +20,9 @@ except ImportError: from sphinx.util import PeekableIterator +# anything below that ratio is considered equal/changed +VERSIONING_RATIO = 65 + def add_uids(doctree, condition): """ Adds a unique id to every node in the `doctree` which matches the condition @@ -88,7 +91,7 @@ def merge_doctrees(old, new, condition): continue else: seen.add(new_node) - if ratio < 65: + if ratio < VERSIONING_RATIO: new_node.uid = old_node.uid else: new_node.uid = uuid4().hex @@ -104,6 +107,8 @@ def get_ratio(old, new): Returns a "similiarity ratio" representing the similarity between the two strings where 0 is equal and anything above less than equal. """ + if not all([old, new]): + return VERSIONING_RATIO return levenshtein_distance(old, new) / (len(old) / 100.0) def levenshtein_distance(a, b): diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 06c4ff2f..923da203 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -16,7 +16,7 @@ from docutils.statemachine import ViewList from docutils.parsers.rst.directives.html import MetaBody from sphinx import addnodes -from sphinx.versioning import add_uids, merge_doctrees +from sphinx.versioning import add_uids, merge_doctrees, get_ratio def setup_module(): global app, original, original_uids @@ -39,6 +39,10 @@ def on_doctree_resolved(app, doctree, docname): def is_paragraph(node): return node.__class__.__name__ == 'paragraph' +def test_get_ratio(): + assert get_ratio('', 'a') + assert get_ratio('a', '') + def test_add_uids(): assert len(original_uids) == 3 -- cgit v1.2.1 From c0089352c76dd4f3064df4e9ec92aa52f07340d5 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Mon, 16 Aug 2010 10:11:52 -0500 Subject: fix moderation test --- tests/test_websupport.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_websupport.py b/tests/test_websupport.py index 3e784405..5ef7a902 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -230,11 +230,13 @@ def moderation_callback(comment): @with_support(moderation_callback=moderation_callback) def test_moderation(support): - raise SkipTest( - 'test is broken, relies on order of test execution and numeric ids') - accepted = support.add_comment('Accepted Comment', node_id=3, + session = Session() + nodes = session.query(Node).all() + node = nodes[7] + session.close() + accepted = support.add_comment('Accepted Comment', node_id=node.id, displayed=False) - rejected = support.add_comment('Rejected comment', node_id=3, + rejected = support.add_comment('Rejected comment', node_id=node.id, displayed=False) # Make sure the moderation_callback is called. assert called == True @@ -243,9 +245,9 @@ def test_moderation(support): raises(UserNotAuthorizedError, support.reject_comment, accepted['id']) support.accept_comment(accepted['id'], moderator=True) support.reject_comment(rejected['id'], moderator=True) - comments = support.get_data(3)['comments'] + comments = support.get_data(node.id)['comments'] assert len(comments) == 1 - comments = support.get_data(3, moderator=True)['comments'] + comments = support.get_data(node.id, moderator=True)['comments'] assert len(comments) == 1 -- cgit v1.2.1 From 59f90293cc7e0b136897f393076500c424f72ec5 Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Wed, 18 Aug 2010 15:41:12 -0500 Subject: display comments inline --- sphinx/themes/basic/static/comment-close.png | Bin 0 -> 3578 bytes sphinx/themes/basic/static/websupport.js | 372 +++++++++++++-------------- sphinx/websupport/__init__.py | 1 + 3 files changed, 184 insertions(+), 189 deletions(-) create mode 100644 sphinx/themes/basic/static/comment-close.png diff --git a/sphinx/themes/basic/static/comment-close.png b/sphinx/themes/basic/static/comment-close.png new file mode 100644 index 00000000..09b54be4 Binary files /dev/null and b/sphinx/themes/basic/static/comment-close.png differ diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index 8f5d07fe..c7ead6fd 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -18,7 +18,7 @@ }; $.fn.autogrow.resize = function(textarea) { - var lineHeight = parseInt($(textarea).css('line-height')); + var lineHeight = parseInt($(textarea).css('line-height'), 10); var lines = textarea.value.split('\n'); var columns = textarea.cols; var lineCount = 0; @@ -31,22 +31,17 @@ })(jQuery); (function($) { - var commentListEmpty, popup, comp; + var comp; function init() { - initTemplates(); initEvents(); initComparator(); - }; + } function initEvents() { - $('a#comment_close').click(function(event) { - event.preventDefault(); - hide(); - }); - $('form#comment_form').submit(function(event) { - event.preventDefault(); - addComment($('form#comment_form')); + $('a.comment_close').live("click", function(event) { + hide($(this).attr('id').substring(2)); + return false; }); $('.vote').live("click", function() { handleVote($(this)); @@ -60,9 +55,9 @@ closeReply($(this).attr('id').substring(2)); return false; }); - $('a.sort_option').click(function(event) { - event.preventDefault(); + $('a.sort_option').live("click", function(event) { handleReSort($(this)); + return false; }); $('a.show_proposal').live("click", function() { showProposal($(this).attr('id').substring(2)); @@ -92,16 +87,27 @@ deleteComment($(this).attr('id').substring(2)); return false; }); - }; + } - function initTemplates() { - // Create our popup div, the same div is recycled each time comments - // are displayed. - popup = $(renderTemplate(popupTemplate, opts)); - // Setup autogrow on the textareas - popup.find('textarea').autogrow(); - $('body').append(popup); - }; + /* + Set comp, which is a comparator function used for sorting and + inserting comments into the list. + */ + function setComparator(by) { + // If the first three letters are "asc", sort in ascending order + // and remove the prefix. + if (by.substring(0,3) == 'asc') { + var i = by.substring(3); + comp = function(a, b) { return a[i] - b[i]; }; + } else { + // Otherwise sort in descending order. + comp = function(a, b) { return b[by] - a[by]; }; + } + + // Reset link styles and format the selected sort option. + $('a.sel').attr('href', '#').removeClass('sel'); + $('#' + by).removeAttr('href').addClass('sel'); + } /* Create a comp function. If the user has preferences stored in @@ -115,59 +121,46 @@ if (start != -1) { start = start + 7; var end = document.cookie.indexOf(";", start); - if (end == -1) + if (end == -1) { end = document.cookie.length; by = unescape(document.cookie.substring(start, end)); } + } } setComparator(by); - }; + } /* - Show the comments popup window. + Show a comment div. */ - function show(nodeId) { - var id = nodeId.substring(1); - - // Reset the main comment form, and set the value of the parent input. - $('form#comment_form') - .find('textarea,input') - .removeAttr('disabled').end() - .find('input[name="node"]') - .val(id).end() - .find('textarea[name="proposal"]') - .val('') - .hide(); - - // Position the popup and show it. - var clientWidth = document.documentElement.clientWidth; - var popupWidth = $('div.popup_comment').width(); - $('div#focuser').fadeIn('fast'); - $('div.popup_comment') - .css({ - 'top': 100 + $(window).scrollTop(), - 'left': clientWidth / 2 - popupWidth / 2, - 'position': 'absolute' - }) - .fadeIn('fast', function() { - getComments(id); - }); - }; + function show(id) { + $('#ao' + id).hide(); + $('#ah' + id).show(); + var context = $.extend({id: id}, opts); + var popup = $(renderTemplate(popupTemplate, context)).hide(); + popup.find('textarea[name="proposal"]').hide(); + var form = popup.find('#cf' + id); + form.submit(function(event) { + event.preventDefault(); + addComment(form); + }); + $('#s' + id).after(popup); + popup.slideDown('fast', function() { + getComments(id); + }); + } /* - Hide the comments popup window. + Hide a comment div. */ - function hide() { - $('div#focuser').fadeOut('fast'); - $('div.popup_comment').fadeOut('fast', function() { - $('ul#comment_ul').empty(); - $('h3#comment_notification').show(); - $('form#comment_form').find('textarea') - .val('').end() - .find('textarea, input') - .removeAttr('disabled'); + function hide(id) { + $('#ah' + id).hide(); + $('#ao' + id).show(); + var div = $('#sc' + id); + div.slideUp('fast', function() { + div.remove(); }); - }; + } /* Perform an ajax request to get comments for a node @@ -179,23 +172,23 @@ url: opts.getCommentsURL, data: {node: id}, success: function(data, textStatus, request) { - var ul = $('ul#comment_ul').hide(); - $('form#comment_form') + var ul = $('#cl' + id); + var speed = 100; + $('#cf' + id) .find('textarea[name="proposal"]') .data('source', data.source); - if (data.comments.length == 0) { + if (data.comments.length === 0) { ul.html('<li>No comments yet.</li>'); - commentListEmpty = true; - var speed = 100; + ul.data('empty', true); } else { // If there are comments, sort them and put them in the list. var comments = sortComments(data.comments); - var speed = data.comments.length * 100; + speed = data.comments.length * 100; appendComments(comments, ul); - commentListEmpty = false; + ul.data('empty', false); } - $('h3#comment_notification').slideUp(speed + 200); + $('#cn' + id).slideUp(speed + 200); ul.slideDown(speed); }, error: function(request, textStatus, error) { @@ -203,7 +196,7 @@ }, dataType: 'json' }); - }; + } /* Add a comment via ajax and insert the comment into the comment tree. @@ -212,6 +205,7 @@ // Disable the form that is being submitted. form.find('textarea,input').attr('disabled', 'disabled'); var node_id = form.find('input[name="node"]').val(); + var parent_id = form.find('input[name="parent"]').val(); // Send the comment to the server. $.ajax({ @@ -220,7 +214,7 @@ dataType: 'json', data: { node: node_id, - parent: form.find('input[name="parent"]').val(), + parent: parent_id, text: form.find('textarea[name="comment"]').val(), proposal: form.find('textarea[name="proposal"]').val() }, @@ -233,9 +227,10 @@ .val('') .add(form.find('input')) .removeAttr('disabled'); - if (commentListEmpty) { - $('ul#comment_ul').empty(); - commentListEmpty = false; + var ul = $('#cl' + (node_id || parent_id)); + if (ul.data('empty')) { + $(ul).empty(); + ul.data('empty', false); } insertComment(data.comment); }, @@ -244,7 +239,7 @@ showError('Oops, there was a problem adding the comment.'); } }); - }; + } /* Recursively append comments to the main comment list and children @@ -259,7 +254,7 @@ this.children = null; div.data('comment', this); }); - }; + } /* After adding a new comment, it must be inserted in the correct @@ -272,13 +267,8 @@ comment.children = null; div.data('comment', comment); - if (comment.node != null) { - var ul = $('ul#comment_ul'); - var siblings = getChildren(ul); - } else { - var ul = $('#cl' + comment.parent); - var siblings = getChildren(ul); - } + var ul = $('#cl' + (comment.node || comment.parent)); + var siblings = getChildren(ul); var li = $(document.createElement('li')); li.hide(); @@ -298,7 +288,7 @@ // or it is the only comment in the list. ul.append(li.html(div)); li.slideDown('fast'); - }; + } function acceptComment(id) { $.ajax({ @@ -310,9 +300,9 @@ }, error: function(request, textStatus, error) { showError("Oops, there was a problem accepting the comment."); - }, + } }); - }; + } function rejectComment(id) { $.ajax({ @@ -327,9 +317,9 @@ }, error: function(request, textStatus, error) { showError("Oops, there was a problem rejecting the comment."); - }, + } }); - }; + } function deleteComment(id) { $.ajax({ @@ -353,54 +343,62 @@ }, error: function(request, textStatus, error) { showError("Oops, there was a problem deleting the comment."); - }, + } }); - }; + } function showProposal(id) { $('#sp' + id).hide(); $('#hp' + id).show(); $('#pr' + id).slideDown('fast'); - }; + } function hideProposal(id) { $('#hp' + id).hide(); $('#sp' + id).show(); $('#pr' + id).slideUp('fast'); - }; + } function showProposeChange(id) { - $('a.show_propose_change').hide(); - $('a.hide_propose_change').show(); - var textarea = $('textarea[name="proposal"]'); + $('#pc' + id).hide(); + $('#hc' + id).show(); + var textarea = $('#pt' + id); textarea.val(textarea.data('source')); $.fn.autogrow.resize(textarea[0]); textarea.slideDown('fast'); - }; + } function hideProposeChange(id) { - $('a.hide_propose_change').hide(); - $('a.show_propose_change').show(); - var textarea = $('textarea[name="proposal"]'); + $('#hc' + id).hide(); + $('#pc' + id).show(); + var textarea = $('#pt' + id); textarea.val('').removeAttr('disabled'); textarea.slideUp('fast'); - }; + } /* Handle when the user clicks on a sort by link. */ function handleReSort(link) { - setComparator(link.attr('id')); + var classes = link.attr('class').split(/\s+/); + var by = ''; + for (var i=0; i<classes.length; i++) { + if (classes[i] != 'sort_option') { + by = classes[i]; + } + } + setComparator(by); // Save/update the sortBy cookie. var expiration = new Date(); expiration.setDate(expiration.getDate() + 365); - document.cookie= 'sortBy=' + escape(link.attr('id')) + + document.cookie= 'sortBy=' + escape(by) + ';expires=' + expiration.toUTCString(); - var comments = getChildren($('ul#comment_ul'), true); - comments = sortComments(comments); - - appendComments(comments, $('ul#comment_ul').empty()); - }; + $('ul.comment_ul').each(function(index, ul) { + var comments = getChildren($(ul), true); + comments = sortComments(comments); + appendComments(comments, $(ul).empty()); + }); + } /* Function to process a vote when a user clicks an arrow. @@ -414,10 +412,9 @@ var id = link.attr('id'); // If it is an unvote, the new vote value is 0, // Otherwise it's 1 for an upvote, or -1 for a downvote. - if (id.charAt(1) == 'u') { - var value = 0; - } else { - var value = id.charAt(0) == 'u' ? 1 : -1; + var value = 0; + if (id.charAt(1) != 'u') { + value = id.charAt(0) == 'u' ? 1 : -1; } // The data to be sent to the server. var d = { @@ -436,13 +433,13 @@ // If this is not an unvote, and the other vote arrow has // already been pressed, unpress it. - if ((d.value != 0) && (data.vote == d.value * -1)) { + if ((d.value !== 0) && (data.vote === d.value * -1)) { $('#' + (d.value == 1 ? 'd' : 'u') + 'u' + d.comment_id).hide(); $('#' + (d.value == 1 ? 'd' : 'u') + 'v' + d.comment_id).show(); } // Update the comments rating in the local data. - data.rating += (data.vote == 0) ? d.value : (d.value - data.vote); + data.rating += (data.vote === 0) ? d.value : (d.value - data.vote); data.vote = d.value; div.data('comment', data); @@ -459,7 +456,7 @@ showError("Oops, there was a problem casting that vote."); } }); - }; + } /* Open a reply form used to reply to an existing comment. @@ -481,7 +478,7 @@ closeReply(id); }); div.slideDown('fast'); - }; + } /* Close the reply form opened with openReply. @@ -495,7 +492,7 @@ // Swap out the hide link for the reply link $('#cr' + id).hide(); $('#rl' + id).show(); - }; + } /* Recursively sort a tree of comments using the comp comparator. @@ -506,27 +503,7 @@ this.children = sortComments(this.children); }); return comments; - }; - - /* - Set comp, which is a comparator function used for sorting and - inserting comments into the list. - */ - function setComparator(by) { - // If the first three letters are "asc", sort in ascending order - // and remove the prefix. - if (by.substring(0,3) == 'asc') { - var i = by.substring(3); - comp = function(a, b) { return a[i] - b[i]; } - } else { - // Otherwise sort in descending order. - comp = function(a, b) { return b[by] - a[by]; } - } - - // Reset link styles and format the selected sort option. - $('a.sel').attr('href', '#').removeClass('sel'); - $('#' + by).removeAttr('href').addClass('sel'); - }; + } /* Get the children comments from a ul. If recursive is true, @@ -543,7 +520,7 @@ children.push(comment); }); return children; - }; + } /* Create a div to display a comment in. @@ -580,7 +557,8 @@ /* A simple template renderer. Placeholders such as <%id%> are replaced - by context['id']. Items are always escaped. + by context['id'] with items being escaped. Placeholders such as <#id#> + are not escaped. */ function renderTemplate(template, context) { var esc = $(document.createElement('div')); @@ -596,16 +574,16 @@ return template.replace(/<([%#])([\w\.]*)\1>/g, function(){ return handle(arguments[2], arguments[1] == '%' ? true : false); }); - }; + } function showError(message) { - $(document.createElement('div')).attr({class: 'popup_error'}) + $(document.createElement('div')).attr({'class': 'popup_error'}) .append($(document.createElement('h1')).text(message)) .appendTo('body') .fadeIn("slow") .delay(2000) .fadeOut("slow"); - }; + } /* Add a link the user uses to open the comments popup. @@ -613,21 +591,42 @@ $.fn.comment = function() { return this.each(function() { var id = $(this).attr('id').substring(1); - var count = COMMENT_METADATA[id] + var count = COMMENT_METADATA[id]; var title = count + ' comment' + (count == 1 ? '' : 's'); var image = count > 0 ? opts.commentBrightImage : opts.commentImage; - $(this).append( - $(document.createElement('a')).attr({href: '#', class: 'sphinx_comment'}) - .append($(document.createElement('img')).attr({ - src: image, - alt: 'comment', - title: title - })) - .click(function(event) { - event.preventDefault(); - show($(this).parent().attr('id')); + $(this) + .append( + $(document.createElement('a')).attr({ + href: '#', + 'class': 'sphinx_comment', + id: 'ao' + id + }) + .append($(document.createElement('img')).attr({ + src: image, + alt: 'comment', + title: title + })) + .click(function(event) { + event.preventDefault(); + show($(this).attr('id').substring(2)); + }) + ) + .append( + $(document.createElement('a')).attr({ + href: '#', + 'class': 'sphinx_comment_close hidden', + id: 'ah' + id }) - ); + .append($(document.createElement('img')).attr({ + src: opts.closeCommentImage, + alt: 'close', + title: 'close' + })) + .click(function(event) { + event.preventDefault(); + hide($(this).attr('id').substring(2)); + }) + ); }); }; @@ -637,8 +636,9 @@ getCommentsURL: '/get_comments', acceptCommentURL: '/accept_comment', rejectCommentURL: '/reject_comment', - rejectCommentURL: '/delete_comment', + deleteCommentURL: '/delete_comment', commentImage: '/static/_static/comment.png', + closeCommentImage: '/static/_static/comment-close.png', loadingImage: '/static/_static/ajax-loader.gif', commentBrightImage: '/static/_static/comment-bright.png', upArrow: '/static/_static/up.png', @@ -715,38 +715,32 @@ </div>'; var popupTemplate = '\ - <div id="popup_template">\ - <div class="popup_comment">\ - <a id="comment_close" href="#">x</a>\ - <h1>Comments</h1>\ - <form method="post" id="comment_form" action="/docs/add_comment">\ - <textarea name="comment" cols="80"></textarea>\ - <p class="propose_button">\ - <a href="#" class="show_propose_change">\ - Propose a change ▹\ - </a>\ - <a href="#" class="hide_propose_change">\ - Propose a change ▿\ - </a>\ - </p>\ - <textarea name="proposal" cols="80" spellcheck="false"></textarea>\ - <input type="submit" value="add comment" id="comment_button" />\ - <input type="hidden" name="node" />\ - <input type="hidden" name="parent" value="" />\ - <p class="sort_options">\ - Sort by:\ - <a href="#" class="sort_option" id="rating">top</a>\ - <a href="#" class="sort_option" id="ascage">newest</a>\ - <a href="#" class="sort_option" id="age">oldest</a>\ - </p>\ - </form>\ - <h3 id="comment_notification">loading comments... <img src="' + - opts.loadingImage + '" alt="" /></h3>\ - <ul id="comment_ul"></ul>\ - </div>\ - </div>\ - <div id="focuser"></div>'; - + <div class="sphinx_comments" id="sc<%id%>">\ + <h1>Comments</h1>\ + <form method="post" id="cf<%id%>" class="comment_form" action="/docs/add_comment">\ + <textarea name="comment" cols="80"></textarea>\ + <p class="propose_button">\ + <a href="#" id="pc<%id%>" class="show_propose_change">\ + Propose a change ▹\ + </a>\ + <a href="#" id="hc<%id%>" class="hide_propose_change">\ + Propose a change ▿\ + </a>\ + </p>\ + <textarea name="proposal" id="pt<%id%>" cols="80" spellcheck="false"></textarea>\ + <input type="submit" value="add comment" />\ + <input type="hidden" name="node" value="<%id%>" />\ + <input type="hidden" name="parent" value="" />\ + <p class="sort_options">\ + Sort by:\ + <a href="#" class="sort_option rating">top</a>\ + <a href="#" class="sort_option ascage">newest</a>\ + <a href="#" class="sort_option age">oldest</a>\ + </p>\ + </form>\ + <h3 id="cn<%id%>">loading comments... <img src="<%loadingImage%>" alt="" /></h3>\ + <ul id="cl<%id%>"></ul>\ + </div>'; $(document).ready(function() { init(); diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index cc065b7f..15251672 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -371,6 +371,7 @@ class WebSupport(object): if self.staticdir != 'static': static_urls = [ ('commentImage', 'comment.png'), + ('closeCommentImage', 'comment-close.png'), ('loadingImage', 'ajax-loader.gif'), ('commentBrightImage', 'comment-bright.png'), ('upArrow', 'up.png'), -- cgit v1.2.1 From 9bc55cab396eb862dade5006c470f6fa8ea3aa3d Mon Sep 17 00:00:00 2001 From: Jacob Mason <jacoblmason@gmail.com> Date: Thu, 19 Aug 2010 11:25:02 -0500 Subject: fix sorting --- sphinx/themes/basic/static/websupport.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sphinx/themes/basic/static/websupport.js b/sphinx/themes/basic/static/websupport.js index c7ead6fd..870b0cdc 100644 --- a/sphinx/themes/basic/static/websupport.js +++ b/sphinx/themes/basic/static/websupport.js @@ -31,7 +31,7 @@ })(jQuery); (function($) { - var comp; + var comp, by; function init() { initEvents(); @@ -93,7 +93,7 @@ Set comp, which is a comparator function used for sorting and inserting comments into the list. */ - function setComparator(by) { + function setComparator() { // If the first three letters are "asc", sort in ascending order // and remove the prefix. if (by.substring(0,3) == 'asc') { @@ -106,7 +106,7 @@ // Reset link styles and format the selected sort option. $('a.sel').attr('href', '#').removeClass('sel'); - $('#' + by).removeAttr('href').addClass('sel'); + $('a.' + by).removeAttr('href').addClass('sel'); } /* @@ -114,7 +114,7 @@ the sortBy cookie, use those, otherwise use the default. */ function initComparator() { - var by = 'rating'; // Default to sort by rating. + by = 'rating'; // Default to sort by rating. // If the sortBy cookie is set, use that instead. if (document.cookie.length > 0) { var start = document.cookie.indexOf('sortBy='); @@ -127,7 +127,7 @@ } } } - setComparator(by); + setComparator(); } /* @@ -139,6 +139,7 @@ var context = $.extend({id: id}, opts); var popup = $(renderTemplate(popupTemplate, context)).hide(); popup.find('textarea[name="proposal"]').hide(); + popup.find('a.' + by).addClass('sel'); var form = popup.find('#cf' + id); form.submit(function(event) { event.preventDefault(); @@ -381,13 +382,12 @@ */ function handleReSort(link) { var classes = link.attr('class').split(/\s+/); - var by = ''; for (var i=0; i<classes.length; i++) { if (classes[i] != 'sort_option') { by = classes[i]; } } - setComparator(by); + setComparator(); // Save/update the sortBy cookie. var expiration = new Date(); expiration.setDate(expiration.getDate() + 365); @@ -739,7 +739,7 @@ </p>\ </form>\ <h3 id="cn<%id%>">loading comments... <img src="<%loadingImage%>" alt="" /></h3>\ - <ul id="cl<%id%>"></ul>\ + <ul id="cl<%id%>" class="comment_ul"></ul>\ </div>'; $(document).ready(function() { -- cgit v1.2.1 From a3204e1529750cb492e9f93650daff9947581a7a Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 19:03:44 +0200 Subject: Small code style changes, remove unused imports. --- sphinx/builders/__init__.py | 1 + sphinx/util/__init__.py | 12 ++++-------- sphinx/versioning.py | 21 +++++++++++---------- sphinx/websupport/__init__.py | 10 ++++++---- sphinx/websupport/search/__init__.py | 20 ++++++++++---------- sphinx/websupport/search/nullsearch.py | 3 ++- sphinx/websupport/search/whooshsearch.py | 4 ++-- sphinx/websupport/search/xapiansearch.py | 3 +-- sphinx/websupport/storage/db.py | 8 ++++---- sphinx/websupport/storage/differ.py | 1 + sphinx/websupport/storage/sqlalchemystorage.py | 14 +++++++++----- 11 files changed, 51 insertions(+), 46 deletions(-) diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 328b2668..1938b361 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -311,6 +311,7 @@ class Builder(object): """ Cleanup any resources. The default implementation does nothing. """ + pass BUILTIN_BUILDERS = { diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index a434f3a8..6a38351f 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -299,6 +299,7 @@ def format_exception_cut_frames(x=1): 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 @@ -312,24 +313,19 @@ class PeekableIterator(object): return self def next(self): - """ - Returns the next item from the iterator. - """ + """Return 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 + """Push 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. - """ + """Return 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 index 06acc63d..430dc142 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -18,15 +18,14 @@ try: except ImportError: from itertools import zip_longest -from sphinx.util import PeekableIterator # anything below that ratio is considered equal/changed VERSIONING_RATIO = 65 + def add_uids(doctree, condition): - """ - Adds a unique id to every node in the `doctree` which matches the condition - and yields it. + """Add a unique id to every node in the `doctree` which matches the + condition and yield the nodes. :param doctree: A :class:`docutils.nodes.document` instance. @@ -38,10 +37,10 @@ def add_uids(doctree, condition): node.uid = uuid4().hex yield node + def merge_doctrees(old, new, condition): - """ - Merges the `old` doctree with the `new` one while looking at nodes matching - the `condition`. + """Merge 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. @@ -102,16 +101,18 @@ def merge_doctrees(old, new, condition): new_node.uid = uuid4().hex yield new_node + def get_ratio(old, new): - """ - Returns a "similiarity ratio" representing the similarity between the two - strings where 0 is equal and anything above less than equal. + """Return a "similiarity ratio" (in percent) representing the similarity + between the two strings where 0 is equal and anything above less than equal. """ if not all([old, new]): return VERSIONING_RATIO return levenshtein_distance(old, new) / (len(old) / 100.0) + def levenshtein_distance(a, b): + """Return the Levenshtein edit distance between two strings *a* and *b*.""" if a == b: return 0 if len(a) < len(b): diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index 15251672..f2d2ba45 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -13,17 +13,17 @@ import sys import cPickle as pickle import posixpath from os import path -from datetime import datetime from jinja2 import Environment, FileSystemLoader from sphinx.application import Sphinx from sphinx.util.osutil import ensuredir from sphinx.util.jsonimpl import dumps as dump_json -from sphinx.websupport.search import BaseSearch, search_adapters +from sphinx.websupport.search import BaseSearch, SEARCH_ADAPTERS from sphinx.websupport.storage import StorageBackend from sphinx.websupport.errors import * + class WebSupportApp(Sphinx): def __init__(self, *args, **kwargs): self.staticdir = kwargs.pop('staticdir', None) @@ -32,6 +32,7 @@ class WebSupportApp(Sphinx): self.storage = kwargs.pop('storage', None) Sphinx.__init__(self, *args, **kwargs) + class WebSupport(object): """The main API class for the web support package. All interactions with the web support package should occur through this class. @@ -82,7 +83,7 @@ class WebSupport(object): if isinstance(search, BaseSearch): self.search = search else: - mod, cls = search_adapters[search or 'null'] + mod, cls = SEARCH_ADAPTERS[search or 'null'] mod = 'sphinx.websupport.search.' + mod SearchClass = getattr(__import__(mod, None, None, [cls]), cls) search_path = path.join(self.datadir, 'search') @@ -390,7 +391,8 @@ class WebSupport(object): :param username: The username of the user making the request. :param moderator: Whether the user making the request is a moderator. """ - parts = [self.base_comment_opts] + # XXX parts is not used? + #parts = [self.base_comment_opts] rv = self.base_comment_opts.copy() if username: rv.update({ diff --git a/sphinx/websupport/search/__init__.py b/sphinx/websupport/search/__init__.py index cb66618b..0cba0f77 100644 --- a/sphinx/websupport/search/__init__.py +++ b/sphinx/websupport/search/__init__.py @@ -11,6 +11,7 @@ import re + class BaseSearch(object): def __init__(self, path): pass @@ -63,11 +64,10 @@ class BaseSearch(object): def query(self, q): """Called by the web support api to get search results. This method - compiles the regular expression to be used when - :meth:`extracting context <extract_context>`, then calls - :meth:`handle_query`. You won't want to override this unless you - don't want to use the included :meth:`extract_context` method. - Override :meth:`handle_query` instead. + compiles the regular expression to be used when :meth:`extracting + context <extract_context>`, then calls :meth:`handle_query`. You + won't want to override this unless you don't want to use the included + :meth:`extract_context` method. Override :meth:`handle_query` instead. :param q: the search query string. """ @@ -93,7 +93,7 @@ class BaseSearch(object): raise NotImplementedError() def extract_context(self, text, length=240): - """Extract the context for the search query from the documents + """Extract the context for the search query from the document's full `text`. :param text: the full text of the document to create the context for @@ -113,9 +113,9 @@ class BaseSearch(object): except TypeError: return context -# The build in search adapters. -search_adapters = { +# The built-in search adapters. +SEARCH_ADAPTERS = { 'xapian': ('xapiansearch', 'XapianSearch'), 'whoosh': ('whooshsearch', 'WhooshSearch'), - 'null': ('nullsearch', 'NullSearch') - } + 'null': ('nullsearch', 'NullSearch'), +} diff --git a/sphinx/websupport/search/nullsearch.py b/sphinx/websupport/search/nullsearch.py index 743983c4..fd6d4dcf 100644 --- a/sphinx/websupport/search/nullsearch.py +++ b/sphinx/websupport/search/nullsearch.py @@ -10,7 +10,8 @@ """ from sphinx.websupport.search import BaseSearch -from sphinx.websupport.errors import * +from sphinx.websupport.errors import NullSearchException + class NullSearch(BaseSearch): """A search adapter that does nothing. Used when no search adapter diff --git a/sphinx/websupport/search/whooshsearch.py b/sphinx/websupport/search/whooshsearch.py index 0f463531..d395dcd7 100644 --- a/sphinx/websupport/search/whooshsearch.py +++ b/sphinx/websupport/search/whooshsearch.py @@ -10,13 +10,13 @@ """ from whoosh import index -from whoosh.fields import Schema, ID, TEXT, STORED +from whoosh.fields import Schema, ID, TEXT from whoosh.analysis import StemmingAnalyzer -from whoosh import highlight from sphinx.util.osutil import ensuredir from sphinx.websupport.search import BaseSearch + class WhooshSearch(BaseSearch): """The whoosh search adapter for sphinx web support.""" diff --git a/sphinx/websupport/search/xapiansearch.py b/sphinx/websupport/search/xapiansearch.py index 16c7e2b1..b0475435 100644 --- a/sphinx/websupport/search/xapiansearch.py +++ b/sphinx/websupport/search/xapiansearch.py @@ -9,13 +9,12 @@ :license: BSD, see LICENSE for details. """ -from os import path - import xapian from sphinx.util.osutil import ensuredir from sphinx.websupport.search import BaseSearch + class XapianSearch(BaseSearch): # Adapted from the GSOC 2009 webapp project. diff --git a/sphinx/websupport/storage/db.py b/sphinx/websupport/storage/db.py index be81a333..bf7b83df 100644 --- a/sphinx/websupport/storage/db.py +++ b/sphinx/websupport/storage/db.py @@ -11,11 +11,9 @@ """ from datetime import datetime -from uuid import uuid4 -from sqlalchemy import Column, Integer, Text, String, Boolean, ForeignKey,\ - DateTime -from sqlalchemy.schema import UniqueConstraint +from sqlalchemy import Column, Integer, Text, String, Boolean, \ + ForeignKey, DateTime from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relation, sessionmaker, aliased @@ -98,6 +96,7 @@ class Node(Base): self.document = document self.source = source + class Comment(Base): """An individual Comment being stored.""" __tablename__ = db_prefix + 'comments' @@ -188,6 +187,7 @@ class Comment(Base): return '%s %s ago' % dt if dt[0] == 1 else '%s %ss ago' % dt + class CommentVote(Base): """A vote a user has made on a Comment.""" __tablename__ = db_prefix + 'commentvote' diff --git a/sphinx/websupport/storage/differ.py b/sphinx/websupport/storage/differ.py index 8d6c4a49..d5225071 100644 --- a/sphinx/websupport/storage/differ.py +++ b/sphinx/websupport/storage/differ.py @@ -13,6 +13,7 @@ import re from cgi import escape from difflib import Differ + class CombinedHtmlDiff(object): """Create an HTML representation of the differences between two pieces of text. diff --git a/sphinx/websupport/storage/sqlalchemystorage.py b/sphinx/websupport/storage/sqlalchemystorage.py index c775f3bb..ba011c06 100644 --- a/sphinx/websupport/storage/sqlalchemystorage.py +++ b/sphinx/websupport/storage/sqlalchemystorage.py @@ -14,16 +14,19 @@ from datetime import datetime from sqlalchemy.orm import aliased from sqlalchemy.sql import func -from sphinx.websupport.errors import * +from sphinx.websupport.errors import CommentNotAllowedError, \ + UserNotAuthorizedError from sphinx.websupport.storage import StorageBackend -from sphinx.websupport.storage.db import Base, Node, Comment, CommentVote,\ - Session +from sphinx.websupport.storage.db import Base, Node, Comment, \ + CommentVote, Session from sphinx.websupport.storage.differ import CombinedHtmlDiff + class SQLAlchemyStorage(StorageBackend): - """A :class:`~sphinx.websupport.storage.StorageBackend` using - SQLAlchemy. """ + A :class:`.StorageBackend` using SQLAlchemy. + """ + def __init__(self, engine): self.engine = engine Base.metadata.bind = engine @@ -147,6 +150,7 @@ class SQLAlchemyStorage(StorageBackend): def accept_comment(self, comment_id): session = Session() + # XXX assignment to "comment" needed? comment = session.query(Comment).filter( Comment.id == comment_id).update( {Comment.displayed: True}) -- cgit v1.2.1 From 33c6ad4dc4b356372e79eedbf8abbfc022f921be Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 19:15:14 +0200 Subject: Adapt style for websupport docs. --- doc/web/api.rst | 83 ++++++----- doc/web/quickstart.rst | 341 ++++++++++++++++++++++---------------------- doc/web/searchadapters.rst | 43 +++--- doc/web/storagebackends.rst | 39 ++--- doc/websupport.rst | 13 +- 5 files changed, 256 insertions(+), 263 deletions(-) diff --git a/doc/web/api.rst b/doc/web/api.rst index b63e6864..070cd3a2 100644 --- a/doc/web/api.rst +++ b/doc/web/api.rst @@ -7,48 +7,47 @@ The WebSupport Class .. class:: WebSupport - The main API class for the web support package. All interactions - with the web support package should occur through this class. - - The class takes the following keyword arguments: - - srcdir - The directory containing reStructuredText source files. - - builddir - The directory that build data and static files should be placed in. - This should be used when creating a :class:`WebSupport` object that - will be used to build data. - - datadir: - The directory that the web support data is in. This should be used - when creating a :class:`WebSupport` object that will be used to - retrieve data. - - search: - This may contain either a string (e.g. 'xapian') referencing a - built-in search adapter to use, or an instance of a subclass of - :class:`~sphinx.websupport.search.BaseSearch`. - - storage: - This may contain either a string representing a database uri, or an - instance of a subclass of - :class:`~sphinx.websupport.storage.StorageBackend`. If this is not - provided a new sqlite database will be created. - - moderation_callback: - A callable to be called when a new comment is added that is not - displayed. It must accept one argument: a dict representing the - comment that was added. - - staticdir: - If static files are served from a location besides "/static", this - should be a string with the name of that location - (e.g. '/static_files'). - - docroot: - If the documentation is not served from the base path of a URL, this - should be a string specifying that path (e.g. 'docs') + The main API class for the web support package. All interactions with the + web support package should occur through this class. + + The class takes the following keyword arguments: + + srcdir + The directory containing reStructuredText source files. + + builddir + The directory that build data and static files should be placed in. This + should be used when creating a :class:`WebSupport` object that will be + used to build data. + + datadir + The directory that the web support data is in. This should be used when + creating a :class:`WebSupport` object that will be used to retrieve data. + + search + This may contain either a string (e.g. 'xapian') referencing a built-in + search adapter to use, or an instance of a subclass of + :class:`~.search.BaseSearch`. + + storage + This may contain either a string representing a database uri, or an + instance of a subclass of :class:`~.storage.StorageBackend`. If this is + not provided, a new sqlite database will be created. + + moderation_callback + A callable to be called when a new comment is added that is not + displayed. It must accept one argument: a dictionary representing the + comment that was added. + + staticdir + If static files are served from a location besides ``'/static'``, this + should be a string with the name of that location + (e.g. ``'/static_files'``). + + docroot + If the documentation is not served from the base path of a URL, this + should be a string specifying that path (e.g. ``'docs'``). + Methods ~~~~~~~ diff --git a/doc/web/quickstart.rst b/doc/web/quickstart.rst index 61a432a8..0627c9c3 100644 --- a/doc/web/quickstart.rst +++ b/doc/web/quickstart.rst @@ -6,52 +6,49 @@ Web Support Quick Start Building Documentation Data ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To make use of the web support package in your application you'll -need to build the data it uses. This data includes pickle files representing -documents, search indices, and node data that is used to track where -comments and other things are in a document. To do this you will need -to create an instance of the :class:`~sphinx.websupport.WebSupport` -class and call it's :meth:`~sphinx.websupport.WebSupport.build` method:: +To make use of the web support package in your application you'll need to build +the data it uses. This data includes pickle files representing documents, +search indices, and node data that is used to track where comments and other +things are in a document. To do this you will need to create an instance of the +:class:`~.WebSupport` class and call its :meth:`~.WebSupport.build` method:: - from sphinx.websupport import WebSupport + from sphinx.websupport import WebSupport - support = WebSupport(srcdir='/path/to/rst/sources/', - builddir='/path/to/build/outdir', - search='xapian') + support = WebSupport(srcdir='/path/to/rst/sources/', + builddir='/path/to/build/outdir', + search='xapian') - support.build() + support.build() -This will read reStructuredText sources from `srcdir` and place the -necessary data in `builddir`. The `builddir` will contain two -sub-directories. One named "data" that contains all the data needed -to display documents, search through documents, and add comments to -documents. The other directory will be called "static" and contains static -files that should be served from "/static". +This will read reStructuredText sources from `srcdir` and place the necessary +data in `builddir`. The `builddir` will contain two sub-directories: one named +"data" that contains all the data needed to display documents, search through +documents, and add comments to documents. The other directory will be called +"static" and contains static files that should be served from "/static". .. note:: - If you wish to serve static files from a path other than "/static", you - can do so by providing the *staticdir* keyword argument when creating - the :class:`~sphinx.websupport.api.WebSupport` object. + If you wish to serve static files from a path other than "/static", you can + do so by providing the *staticdir* keyword argument when creating the + :class:`~.WebSupport` object. + Integrating Sphinx Documents Into Your Webapp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Now that the data is built, it's time to do something useful with it. -Start off by creating a :class:`~sphinx.websupport.WebSupport` object -for your application:: +Now that the data is built, it's time to do something useful with it. Start off +by creating a :class:`~.WebSupport` object for your application:: - from sphinx.websupport import WebSupport + from sphinx.websupport import WebSupport - support = WebSupport(datadir='/path/to/the/data', - search='xapian') + support = WebSupport(datadir='/path/to/the/data', + search='xapian') -You'll only need one of these for each set of documentation you will be -working with. You can then call it's -:meth:`~sphinx.websupport.WebSupport.get_document` method to access +You'll only need one of these for each set of documentation you will be working +with. You can then call it's :meth:`~.WebSupport.get_document` method to access individual documents:: - contents = support.get_document('contents') + contents = support.get_document('contents') This will return a dictionary containing the following items: @@ -62,132 +59,132 @@ This will return a dictionary containing the following items: * **css**: Links to css files used by Sphinx * **js**: Javascript containing comment options -This dict can then be used as context for templates. The goal is to be -easy to integrate with your existing templating system. An example using -`Jinja2 <http://jinja.pocoo.org/2/>`_ is: +This dict can then be used as context for templates. The goal is to be easy to +integrate with your existing templating system. An example using `Jinja2 +<http://jinja.pocoo.org/2/>`_ is: .. sourcecode:: html+jinja - {%- extends "layout.html" %} + {%- extends "layout.html" %} + + {%- block title %} + {{ document.title }} + {%- endblock %} - {%- block title %} - {{ document.title }} - {%- endblock %} + {% block css %} + {{ super() }} + {{ document.css|safe }} + <link rel="stylesheet" href="/static/websupport-custom.css" type="text/css"> + {% endblock %} - {% block css %} - {{ super() }} - {{ document.css|safe }} - <link rel="stylesheet" href="/static/websupport-custom.css" type="text/css"> - {% endblock %} + {%- block js %} + {{ super() }} + {{ document.js|safe }} + {%- endblock %} - {%- block js %} - {{ super() }} - {{ document.js|safe }} - {%- endblock %} + {%- block relbar %} + {{ document.relbar|safe }} + {%- endblock %} - {%- block relbar %} - {{ document.relbar|safe }} - {%- endblock %} + {%- block body %} + {{ document.body|safe }} + {%- endblock %} - {%- block body %} - {{ document.body|safe }} - {%- endblock %} + {%- block sidebar %} + {{ document.sidebar|safe }} + {%- endblock %} - {%- block sidebar %} - {{ document.sidebar|safe }} - {%- endblock %} Authentication -------------- -To use certain features such as voting it must be possible to authenticate -users. The details of the authentication are left to your application. -Once a user has been authenticated you can pass the user's details to certain -:class:`~sphinx.websupport.WebSupport` methods using the *username* and -*moderator* keyword arguments. The web support package will store the -username with comments and votes. The only caveat is that if you allow users -to change their username you must update the websupport package's data:: +To use certain features such as voting, it must be possible to authenticate +users. The details of the authentication are left to your application. Once a +user has been authenticated you can pass the user's details to certain +:class:`~.WebSupport` methods using the *username* and *moderator* keyword +arguments. The web support package will store the username with comments and +votes. The only caveat is that if you allow users to change their username you +must update the websupport package's data:: - support.update_username(old_username, new_username) + support.update_username(old_username, new_username) *username* should be a unique string which identifies a user, and *moderator* -should be a boolean representing whether the user has moderation -privilieges. The default value for *moderator* is *False*. - -An example `Flask <http://flask.pocoo.org/>`_ function that checks whether -a user is logged in and then retrieves a document is:: - - from sphinx.websupport.errors import * - - @app.route('/<path:docname>') - def doc(docname): - username = g.user.name if g.user else '' - moderator = g.user.moderator if g.user else False - try: - document = support.get_document(docname, username, moderator) - except DocumentNotFoundError: - abort(404) - return render_template('doc.html', document=document) - -The first thing to notice is that the *docname* is just the request path. -This makes accessing the correct document easy from a single view. -If the user is authenticated then the username and moderation status are -passed along with the docname to -:meth:`~sphinx.websupport.WebSupport.get_document`. The web support package -will then add this data to the COMMENT_OPTIONS that are used in the template. +should be a boolean representing whether the user has moderation privilieges. +The default value for *moderator* is *False*. + +An example `Flask <http://flask.pocoo.org/>`_ function that checks whether a +user is logged in and then retrieves a document is:: + + from sphinx.websupport.errors import * + + @app.route('/<path:docname>') + def doc(docname): + username = g.user.name if g.user else '' + moderator = g.user.moderator if g.user else False + try: + document = support.get_document(docname, username, moderator) + except DocumentNotFoundError: + abort(404) + return render_template('doc.html', document=document) + +The first thing to notice is that the *docname* is just the request path. This +makes accessing the correct document easy from a single view. If the user is +authenticated, then the username and moderation status are passed along with the +docname to :meth:`~.WebSupport.get_document`. The web support package will then +add this data to the ``COMMENT_OPTIONS`` that are used in the template. .. note:: - This only works works if your documentation is served from your - document root. If it is served from another directory, you will - need to prefix the url route with that directory, and give the `docroot` - keyword argument when creating the web support object:: + This only works works if your documentation is served from your + document root. If it is served from another directory, you will + need to prefix the url route with that directory, and give the `docroot` + keyword argument when creating the web support object:: + + support = WebSupport(..., docroot='docs') - support = WebSupport(..., docroot='docs') + @app.route('/docs/<path:docname>') - @app.route('/docs/<path:docname>') Performing Searches ~~~~~~~~~~~~~~~~~~~ -To use the search form built-in to the Sphinx sidebar, create a function -to handle requests to the url 'search' relative to the documentation root. -The user's search query will be in the GET parameters, with the key `q`. -Then use the :meth:`~sphinx.websupport.WebSupport.get_search_results` method -to retrieve search results. In `Flask <http://flask.pocoo.org/>`_ that -would be like this:: - - @app.route('/search') - def search(): - q = request.args.get('q') - document = support.get_search_results(q) - return render_template('doc.html', document=document) - -Note that we used the same template to render our search results as we -did to render our documents. That's because -:meth:`~sphinx.websupport.WebSupport.get_search_results` returns a context -dict in the same format that -:meth:`~sphinx.websupport.WebSupport.get_document` does. +To use the search form built-in to the Sphinx sidebar, create a function to +handle requests to the url 'search' relative to the documentation root. The +user's search query will be in the GET parameters, with the key `q`. Then use +the :meth:`~sphinx.websupport.WebSupport.get_search_results` method to retrieve +search results. In `Flask <http://flask.pocoo.org/>`_ that would be like this:: + + @app.route('/search') + def search(): + q = request.args.get('q') + document = support.get_search_results(q) + return render_template('doc.html', document=document) + +Note that we used the same template to render our search results as we did to +render our documents. That's because :meth:`~.WebSupport.get_search_results` +returns a context dict in the same format that :meth:`~.WebSupport.get_document` +does. + Comments & Proposals ~~~~~~~~~~~~~~~~~~~~ -Now that this is done it's time to define the functions that handle -the AJAX calls from the script. You will need three functions. The first -function is used to add a new comment, and will call the web support method -:meth:`~sphinx.websupport.WebSupport.add_comment`:: - - @app.route('/docs/add_comment', methods=['POST']) - def add_comment(): - parent_id = request.form.get('parent', '') - node_id = request.form.get('node', '') - text = request.form.get('text', '') - proposal = request.form.get('proposal', '') - username = g.user.name if g.user is not None else 'Anonymous' - comment = support.add_comment(text, node_id='node_id', - parent_id='parent_id', - username=username, proposal=proposal) - return jsonify(comment=comment) +Now that this is done it's time to define the functions that handle the AJAX +calls from the script. You will need three functions. The first function is +used to add a new comment, and will call the web support method +:meth:`~.WebSupport.add_comment`:: + + @app.route('/docs/add_comment', methods=['POST']) + def add_comment(): + parent_id = request.form.get('parent', '') + node_id = request.form.get('node', '') + text = request.form.get('text', '') + proposal = request.form.get('proposal', '') + username = g.user.name if g.user is not None else 'Anonymous' + comment = support.add_comment(text, node_id='node_id', + parent_id='parent_id', + username=username, proposal=proposal) + return jsonify(comment=comment) You'll notice that both a `parent_id` and `node_id` are sent with the request. If the comment is being attached directly to a node, `parent_id` @@ -204,63 +201,61 @@ specific node, and is aptly named data = support.get_data(parent_id, user_id) return jsonify(**data) -The final function that is needed will call -:meth:`~sphinx.websupport.WebSupport.process_vote`, and will handle user -votes on comments:: - - @app.route('/docs/process_vote', methods=['POST']) - def process_vote(): - if g.user is None: - abort(401) - comment_id = request.form.get('comment_id') - value = request.form.get('value') - if value is None or comment_id is None: - abort(400) - support.process_vote(comment_id, g.user.id, value) - return "success" +The final function that is needed will call :meth:`~.WebSupport.process_vote`, +and will handle user votes on comments:: + + @app.route('/docs/process_vote', methods=['POST']) + def process_vote(): + if g.user is None: + abort(401) + comment_id = request.form.get('comment_id') + value = request.form.get('value') + if value is None or comment_id is None: + abort(400) + support.process_vote(comment_id, g.user.id, value) + return "success" + Comment Moderation ~~~~~~~~~~~~~~~~~~ -By default all comments added through -:meth:`~sphinx.websupport.WebSupport.add_comment` are automatically -displayed. If you wish to have some form of moderation, you can pass -the `displayed` keyword argument:: +By default, all comments added through :meth:`~.WebSupport.add_comment` are +automatically displayed. If you wish to have some form of moderation, you can +pass the `displayed` keyword argument:: - comment = support.add_comment(text, node_id='node_id', - parent_id='parent_id', - username=username, proposal=proposal, - displayed=False) + comment = support.add_comment(text, node_id='node_id', + parent_id='parent_id', + username=username, proposal=proposal, + displayed=False) -You can then create two new views to handle the moderation of comments. The -first will be called when a moderator decides a comment should be accepted -and displayed:: +You can then create two new views to handle the moderation of comments. The +first will be called when a moderator decides a comment should be accepted and +displayed:: - @app.route('/docs/accept_comment', methods=['POST']) - def accept_comment(): - moderator = g.user.moderator if g.user else False - comment_id = request.form.get('id') - support.accept_comment(comment_id, moderator=moderator) - return 'OK' + @app.route('/docs/accept_comment', methods=['POST']) + def accept_comment(): + moderator = g.user.moderator if g.user else False + comment_id = request.form.get('id') + support.accept_comment(comment_id, moderator=moderator) + return 'OK' The next is very similar, but used when rejecting a comment:: - @app.route('/docs/reject_comment', methods=['POST']) - def reject_comment(): - moderator = g.user.moderator if g.user else False - comment_id = request.form.get('id') - support.reject_comment(comment_id, moderator=moderator) - return 'OK' + @app.route('/docs/reject_comment', methods=['POST']) + def reject_comment(): + moderator = g.user.moderator if g.user else False + comment_id = request.form.get('id') + support.reject_comment(comment_id, moderator=moderator) + return 'OK' -To perform a custom action (such as emailing a moderator) when a new comment -is added but not displayed, you can pass callable to the -:class:`~sphinx.websupport.WebSupport` class when instantiating your support -object:: +To perform a custom action (such as emailing a moderator) when a new comment is +added but not displayed, you can pass callable to the :class:`~.WebSupport` +class when instantiating your support object:: - def moderation_callback(comment): - """Do something...""" + def moderation_callback(comment): + """Do something...""" - support = WebSupport(..., moderation_callback=moderation_callback) + support = WebSupport(..., moderation_callback=moderation_callback) -The moderation callback must take one argument, which will be the same -comment dict that is returned by add_comment. +The moderation callback must take one argument, which will be the same comment +dict that is returned by :meth:`add_comment`. diff --git a/doc/web/searchadapters.rst b/doc/web/searchadapters.rst index a84aa8da..7d8634f7 100644 --- a/doc/web/searchadapters.rst +++ b/doc/web/searchadapters.rst @@ -6,41 +6,40 @@ Search Adapters =============== To create a custom search adapter you will need to subclass the -:class:`~BaseSearch` class. Then create an instance of the new class -and pass that as the `search` keyword argument when you create the -:class:`~sphinx.websupport.WebSupport` object:: +:class:`BaseSearch` class. Then create an instance of the new class and pass +that as the `search` keyword argument when you create the :class:`~.WebSupport` +object:: - support = Websupport(srcdir=srcdir, - builddir=builddir, - search=MySearch()) + support = WebSupport(srcdir=srcdir, + builddir=builddir, + search=MySearch()) -For more information about creating a custom search adapter, please see -the documentation of the :class:`BaseSearch` class below. +For more information about creating a custom search adapter, please see the +documentation of the :class:`BaseSearch` class below. .. class:: BaseSearch - Defines an interface for search adapters. + Defines an interface for search adapters. + BaseSearch Methods ~~~~~~~~~~~~~~~~~~ - The following methods are defined in the BaseSearch class. Some methods - do not need to be overridden, but some ( - :meth:`~sphinx.websupport.search.BaseSearch.add_document` and - :meth:`~sphinx.websupport.search.BaseSearch.handle_query`) must be - overridden in your subclass. For a working example, look at the - built-in adapter for whoosh. + The following methods are defined in the BaseSearch class. Some methods do + not need to be overridden, but some (:meth:`~BaseSearch.add_document` and + :meth:`~BaseSearch.handle_query`) must be overridden in your subclass. For a + working example, look at the built-in adapter for whoosh. -.. automethod:: sphinx.websupport.search.BaseSearch.init_indexing +.. automethod:: BaseSearch.init_indexing -.. automethod:: sphinx.websupport.search.BaseSearch.finish_indexing +.. automethod:: BaseSearch.finish_indexing -.. automethod:: sphinx.websupport.search.BaseSearch.feed +.. automethod:: BaseSearch.feed -.. automethod:: sphinx.websupport.search.BaseSearch.add_document +.. automethod:: BaseSearch.add_document -.. automethod:: sphinx.websupport.search.BaseSearch.query +.. automethod:: BaseSearch.query -.. automethod:: sphinx.websupport.search.BaseSearch.handle_query +.. automethod:: BaseSearch.handle_query -.. automethod:: sphinx.websupport.search.BaseSearch.extract_context +.. automethod:: BaseSearch.extract_context diff --git a/doc/web/storagebackends.rst b/doc/web/storagebackends.rst index 6b701ea3..a46ea9e5 100644 --- a/doc/web/storagebackends.rst +++ b/doc/web/storagebackends.rst @@ -6,40 +6,41 @@ Storage Backends ================ To create a custom storage backend you will need to subclass the -:class:`~StorageBackend` class. Then create an instance of the new class -and pass that as the `storage` keyword argument when you create the -:class:`~sphinx.websupport.WebSupport` object:: +:class:`StorageBackend` class. Then create an instance of the new class and +pass that as the `storage` keyword argument when you create the +:class:`~.WebSupport` object:: - support = Websupport(srcdir=srcdir, - builddir=builddir, - storage=MyStorage()) + support = WebSupport(srcdir=srcdir, + builddir=builddir, + storage=MyStorage()) -For more information about creating a custom storage backend, please see -the documentation of the :class:`StorageBackend` class below. +For more information about creating a custom storage backend, please see the +documentation of the :class:`StorageBackend` class below. .. class:: StorageBackend - Defines an interface for storage backends. + Defines an interface for storage backends. + StorageBackend Methods ~~~~~~~~~~~~~~~~~~~~~~ -.. automethod:: sphinx.websupport.storage.StorageBackend.pre_build +.. automethod:: StorageBackend.pre_build -.. automethod:: sphinx.websupport.storage.StorageBackend.add_node +.. automethod:: StorageBackend.add_node -.. automethod:: sphinx.websupport.storage.StorageBackend.post_build +.. automethod:: StorageBackend.post_build -.. automethod:: sphinx.websupport.storage.StorageBackend.add_comment +.. automethod:: StorageBackend.add_comment -.. automethod:: sphinx.websupport.storage.StorageBackend.delete_comment +.. automethod:: StorageBackend.delete_comment -.. automethod:: sphinx.websupport.storage.StorageBackend.get_data +.. automethod:: StorageBackend.get_data -.. automethod:: sphinx.websupport.storage.StorageBackend.process_vote +.. automethod:: StorageBackend.process_vote -.. automethod:: sphinx.websupport.storage.StorageBackend.update_username +.. automethod:: StorageBackend.update_username -.. automethod:: sphinx.websupport.storage.StorageBackend.accept_comment +.. automethod:: StorageBackend.accept_comment -.. automethod:: sphinx.websupport.storage.StorageBackend.reject_comment +.. automethod:: StorageBackend.reject_comment diff --git a/doc/websupport.rst b/doc/websupport.rst index 4d743719..7dbea03b 100644 --- a/doc/websupport.rst +++ b/doc/websupport.rst @@ -3,13 +3,12 @@ Sphinx Web Support ================== -Sphinx provides a way to easily integrate Sphinx documentation -into your web application. To learn more read the -:ref:`websupportquickstart`. +Sphinx provides a Python API to easily integrate Sphinx documentation into your +web application. To learn more read the :ref:`websupportquickstart`. .. toctree:: - web/quickstart - web/api - web/searchadapters - web/storagebackends + web/quickstart + web/api + web/searchadapters + web/storagebackends -- cgit v1.2.1 From 9038481cb01b79b21199ee0d3d3d54a1e69680ac Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 19:19:12 +0200 Subject: Code style nits. --- sphinx/builders/intl.py | 2 -- sphinx/environment.py | 8 ++++---- sphinx/locale/__init__.py | 6 +++--- sphinx/util/nodes.py | 1 + 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index de147c82..41943b9b 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -13,8 +13,6 @@ import collections from datetime import datetime from os import path -from docutils import nodes - from sphinx.builders import Builder from sphinx.util.nodes import extract_messages from sphinx.util.osutil import SEP diff --git a/sphinx/environment.py b/sphinx/environment.py index 6339675b..f5d77a1e 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -184,6 +184,7 @@ class CitationReferences(Transform): refnode += nodes.Text('[' + cittext + ']') citnode.parent.replace(citnode, refnode) + class Locale(Transform): """ Replace translatable nodes with their translated doctree. @@ -207,10 +208,11 @@ class Locale(Transform): parser = RSTParser() for node, msg in extract_messages(self.document): - ctx = node.parent + # XXX ctx not used + #ctx = node.parent patch = new_document(source, settings) msgstr = catalog.ugettext(msg) - #XXX add marker to untranslated parts + # XXX add marker to untranslated parts if not msgstr or msgstr == msg: # as-of-yet untranslated continue parser.parse(msgstr, patch) @@ -221,7 +223,6 @@ class Locale(Transform): node.children = patch.children - class SphinxStandaloneReader(standalone.Reader): """ Add our own transforms. @@ -792,7 +793,6 @@ class BuildEnvironment: if node['level'] < filterlevel: node.parent.remove(node) - def process_dependencies(self, docname, doctree): """ Process docutils-generated dependency info. diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py index 682fdc6f..472fbed6 100644 --- a/sphinx/locale/__init__.py +++ b/sphinx/locale/__init__.py @@ -191,9 +191,9 @@ else: def init(locale_dirs, language, catalog='sphinx'): """ Look for message catalogs in `locale_dirs` and *ensure* that there is at - least a NullTranslations catalog set in `translators`. If called multiple - times or several ``.mo`` files are found their contents are merged - together (thus making `init` reentrable). + least a NullTranslations catalog set in `translators`. If called multiple + times or if several ``.mo`` files are found, their contents are merged + together (thus making ``init`` reentrable). """ global translators translator = translators.get(catalog) diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index c2f96f07..9ecf0d4a 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -28,6 +28,7 @@ def extract_messages(doctree): if isinstance(node, (nodes.Invisible, nodes.Inline)): continue # <field_name>orphan</field_name> + # XXX ignore all metadata (== docinfo) if isinstance(node, nodes.field_name) and node.children[0] == 'orphan': continue msg = node.rawsource.replace('\n', ' ').strip() -- cgit v1.2.1 From 5ce1455eaf2dccede1a08d2f54bd69d4d5c54fe8 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 19:23:05 +0200 Subject: Update versionadded for SoC projects. --- doc/intl.rst | 13 ++++++++----- doc/websupport.rst | 2 ++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/doc/intl.rst b/doc/intl.rst index 8722177d..d8c5ec78 100644 --- a/doc/intl.rst +++ b/doc/intl.rst @@ -3,9 +3,12 @@ Internationalization ==================== -.. versionadded:: 1.XXX +.. versionadded:: 1.1 -Complementary to translations provided for internal messages such as navigation -bars Sphinx provides mechanisms facilitating *document* translations in itself. -It relies on the existing configuration values :confval:`language` and -:confval:`locale_dirs`. +Complementary to translations provided for Sphinx-generated messages such as +navigation bars Sphinx provides mechanisms facilitating *document* translations +in itself. It relies on the existing configuration values :confval:`language` +and :confval:`locale_dirs`. + + +.. XXX write more! diff --git a/doc/websupport.rst b/doc/websupport.rst index 7dbea03b..3ccae246 100644 --- a/doc/websupport.rst +++ b/doc/websupport.rst @@ -3,6 +3,8 @@ Sphinx Web Support ================== +.. versionadded:: 1.1 + Sphinx provides a Python API to easily integrate Sphinx documentation into your web application. To learn more read the :ref:`websupportquickstart`. -- cgit v1.2.1 From 60b9f5c9c04c6cf03379ebbce19db1cc6b0a50b0 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sat, 21 Aug 2010 19:54:14 +0200 Subject: Added missing import --- sphinx/builders/intl.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sphinx/builders/intl.py b/sphinx/builders/intl.py index 49993b80..1572747e 100644 --- a/sphinx/builders/intl.py +++ b/sphinx/builders/intl.py @@ -14,6 +14,8 @@ from codecs import open from datetime import datetime from collections import defaultdict +from docutils import nodes + from sphinx.builders import Builder from sphinx.builders.versioning import VersioningBuilderMixin from sphinx.util.nodes import extract_messages -- cgit v1.2.1 From 232a6b128200d1162c1e4026ba3569b9b866b535 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 20:30:11 +0200 Subject: Give an explicit locale so that the output file name of msginit is deterministic. --- tests/test_build_gettext.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index 03291111..ba2440fd 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -37,7 +37,8 @@ def test_gettext(app): os.chdir(app.outdir) try: try: - p = Popen(['msginit', '--no-translator', '-i', 'markup.pot'], + p = Popen(['msginit', '--no-translator', '-i', 'markup.pot', + '--locale', 'en_US'], stdout=PIPE, stderr=PIPE) except OSError: return # most likely msginit was not found @@ -74,15 +75,6 @@ def test_gettext(app): def test_all(app): app.builder.build_all() -@with_app(buildername='text', - confoverrides={'language': 'xx', 'locale_dirs': ['.']}) -def test_patch(app): - app.builder.build(['bom']) - result = (app.outdir / 'bom.txt').text(encoding='utf-8') - expect = (u"\nDatei mit UTF-8" - u"\n***************\n" # underline matches new translation - u"\nThis file has umlauts: äöü.\n") - assert result == expect def setup_patch(): (test_root / 'xx' / 'LC_MESSAGES').makedirs() @@ -103,5 +95,16 @@ def setup_patch(): def teardown_patch(): (test_root / 'xx').rmtree() + +@with_app(buildername='text', + confoverrides={'language': 'xx', 'locale_dirs': ['.']}) +def test_patch(app): + app.builder.build(['bom']) + result = (app.outdir / 'bom.txt').text(encoding='utf-8') + expect = (u"\nDatei mit UTF-8" + u"\n***************\n" # underline matches new translation + u"\nThis file has umlauts: äöü.\n") + assert result == expect + test_patch.setup = setup_patch test_patch.teardown = teardown_patch -- cgit v1.2.1 From 9ed870e653971a8176b135e601023e73f56f4d6d Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sat, 21 Aug 2010 20:53:05 +0200 Subject: Added a fallback for itertools product to pycompat for python versions < 2.6 --- sphinx/util/pycompat.py | 12 ++++++++++++ sphinx/versioning.py | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index 5f23bbe1..faebcd01 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -21,6 +21,18 @@ except ImportError: # Python 3 class_types = (type,) +try: + from itertools import product +except ImportError: # python < 2.6 + # this code has been taken from the python documentation + def product(*args, **kwargs): + pools = map(tuple, args) * kwargs.get('repeat', 1) + result = [[]] + for pool in pools: + result = [x + [y] for x in result for y in pool] + for prod in result: + yield tuple(prod) + # the ubiquitous "bytes" helper function if sys.version_info >= (3, 0): diff --git a/sphinx/versioning.py b/sphinx/versioning.py index 430dc142..0ea494f0 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -12,12 +12,13 @@ from uuid import uuid4 from operator import itemgetter from collections import defaultdict -from itertools import product try: from itertools import izip_longest as zip_longest except ImportError: from itertools import zip_longest +from sphinx.util.pycompat import product + # anything below that ratio is considered equal/changed VERSIONING_RATIO = 65 -- cgit v1.2.1 From f883c953ca5f077c5dfc7c31217ea2bd3c2b0bcf Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Sat, 21 Aug 2010 21:08:15 +0200 Subject: Added a fallback for itertools.izip_longest to pycompat for python versions < 2.6 --- sphinx/util/pycompat.py | 21 +++++++++++++++++++++ sphinx/versioning.py | 6 +----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index faebcd01..bfe96a98 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -33,6 +33,27 @@ except ImportError: # python < 2.6 for prod in result: yield tuple(prod) +try: + from itertools import izip_longest as zip_longest +except ImportError: # python > 2.6/2.7 or python < 2.6 + try: + from itertools import zip_longest + except ImportError: # python < 2.6 + # this code has been taken from the python documentation + from itertools import repeat, chain, izip + + def zip_longest(*args, **kwargs): + fillvalue = kwargs.get('fillvalue') + def sentinel(counter=([fillvalue] * (len(args) - 1)).pop): + yield counter() + fillers = repeat(fillvalue) + iters = [chain(it, sentinel(), fillers) for it in args] + try: + for tup in izip(*iters): + yield tup + except IndexError: + pass + # the ubiquitous "bytes" helper function if sys.version_info >= (3, 0): diff --git a/sphinx/versioning.py b/sphinx/versioning.py index 0ea494f0..5b0b2127 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -12,12 +12,8 @@ from uuid import uuid4 from operator import itemgetter from collections import defaultdict -try: - from itertools import izip_longest as zip_longest -except ImportError: - from itertools import zip_longest -from sphinx.util.pycompat import product +from sphinx.util.pycompat import product, zip_longest # anything below that ratio is considered equal/changed -- cgit v1.2.1 From 3596d3cea54846a6a0c0ae0151ce0a5d2c8f2e28 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 21:54:50 +0200 Subject: Supply substitute implementation of itertools.(i)zip_longest. --- sphinx/util/pycompat.py | 29 ++++++++++++++++++++++++++--- sphinx/versioning.py | 6 +----- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index faebcd01..cba4ec14 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -12,7 +12,7 @@ import sys import codecs import encodings -import re + try: from types import ClassType @@ -21,10 +21,11 @@ except ImportError: # Python 3 class_types = (type,) + try: from itertools import product -except ImportError: # python < 2.6 - # this code has been taken from the python documentation +except ImportError: # Python < 2.6 + # this code has been taken from the Python itertools documentation def product(*args, **kwargs): pools = map(tuple, args) * kwargs.get('repeat', 1) result = [[]] @@ -34,6 +35,28 @@ except ImportError: # python < 2.6 yield tuple(prod) +try: + from itertools import izip_longest as zip_longest +except ImportError: # Python < 2.6 or >= 3.0 + try: + from itertools import zip_longest + except ImportError: + from itertools import izip, repeat, chain + # this code has been taken from the Python itertools documentation + def izip_longest(*args, **kwds): + # izip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D- + fillvalue = kwds.get('fillvalue') + def sentinel(counter = ([fillvalue]*(len(args)-1)).pop): + yield counter() # yields the fillvalue, or raises IndexError + fillers = repeat(fillvalue) + iters = [chain(it, sentinel(), fillers) for it in args] + try: + for tup in izip(*iters): + yield tup + except IndexError: + pass + + # the ubiquitous "bytes" helper function if sys.version_info >= (3, 0): def b(s): diff --git a/sphinx/versioning.py b/sphinx/versioning.py index 0ea494f0..5b0b2127 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -12,12 +12,8 @@ from uuid import uuid4 from operator import itemgetter from collections import defaultdict -try: - from itertools import izip_longest as zip_longest -except ImportError: - from itertools import zip_longest -from sphinx.util.pycompat import product +from sphinx.util.pycompat import product, zip_longest # anything below that ratio is considered equal/changed -- cgit v1.2.1 From c801dac6ae6ab4bb6f7bef458ac0f2e71b678f0c Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 22:01:42 +0200 Subject: Fix version. --- tests/test_websupport.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_websupport.py b/tests/test_websupport.py index 5ef7a902..bf66cb3b 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -12,6 +12,12 @@ import os from StringIO import StringIO +try: + from functools import wraps +except ImportError: + # functools is new in 2.5 + wraps = lambda f: (lambda w: w) + from nose import SkipTest from sphinx.websupport import WebSupport @@ -20,13 +26,8 @@ from sphinx.websupport.storage.differ import CombinedHtmlDiff from sphinx.websupport.storage.sqlalchemystorage import Session, \ SQLAlchemyStorage, Comment, CommentVote from sphinx.websupport.storage.db import Node -from util import * -try: - from functools import wraps -except ImportError: - # functools is new in 2.4 - wraps = lambda f: (lambda w: w) +from util import * default_settings = {'builddir': os.path.join(test_root, 'websupport'), -- cgit v1.2.1 From eee1cc940ad300355e26c1fa91cd202736535bf4 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 22:11:01 +0200 Subject: Group conditional sections in pycompat. --- sphinx/util/pycompat.py | 137 +++++++++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 72 deletions(-) diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index 5ae59907..e5a712c9 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -13,64 +13,17 @@ import sys import codecs import encodings +# ------------------------------------------------------------------------------ +# Python 2/3 compatibility -try: - from types import ClassType - class_types = (type, ClassType) -except ImportError: +if sys.version_info >= (3, 0): # Python 3 class_types = (type,) - - -try: - from itertools import product -except ImportError: # Python < 2.6 - # this code has been taken from the Python itertools documentation - def product(*args, **kwargs): - pools = map(tuple, args) * kwargs.get('repeat', 1) - result = [[]] - for pool in pools: - result = [x + [y] for x in result for y in pool] - for prod in result: - yield tuple(prod) - - -try: - from itertools import izip_longest as zip_longest -except ImportError: # Python < 2.6 or >= 3.0 - try: - from itertools import zip_longest - except ImportError: - from itertools import izip, repeat, chain - # this code has been taken from the Python itertools documentation - def zip_longest(*args, **kwds): - # zip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D- - fillvalue = kwds.get('fillvalue') - def sentinel(counter = ([fillvalue]*(len(args)-1)).pop): - yield counter() # yields the fillvalue, or raises IndexError - fillers = repeat(fillvalue) - iters = [chain(it, sentinel(), fillers) for it in args] - try: - for tup in izip(*iters): - yield tup - except IndexError: - pass - - -# the ubiquitous "bytes" helper function -if sys.version_info >= (3, 0): + # the ubiquitous "bytes" helper functions def b(s): return s.encode('utf-8') -else: - b = str - - -# Support for running 2to3 over config files - -if sys.version_info < (3, 0): - # no need to refactor on 2.x versions - convert_with_2to3 = None -else: + bytes = bytes + # support for running 2to3 over config files def convert_with_2to3(filepath): from lib2to3.refactor import RefactoringTool, get_fixers_from_package from lib2to3.pgen2.parse import ParseError @@ -86,32 +39,74 @@ else: raise SyntaxError(err.msg, (filepath, lineno, offset, err.value)) return unicode(tree) +else: + # Python 2 + from types import ClassType + class_types = (type, ClassType) + b = str + bytes = str + # no need to refactor on 2.x versions + convert_with_2to3 = None -try: - base_exception = BaseException -except NameError: - base_exception = Exception +# ------------------------------------------------------------------------------ +# Missing itertools in Python < 2.6 -try: - next = next -except NameError: - # this is on Python 2, where the method is called "next" (it is refactored - # to __next__ by 2to3, but in that case never executed) - def next(iterator): - return iterator.next() +if sys.version_info >= (2, 6): + # Python >= 2.6 + from itertools import product + try: + from itertools import zip_longest # Python 3 name + except ImportError: + from itertools import izip_longest as zip_longest +else: + # Python < 2.6 + from itertools import izip, repeat, chain -try: - bytes = bytes -except NameError: - bytes = str + # These replacement functions have been taken from the Python 2.6 + # itertools documentation. + def product(*args, **kwargs): + pools = map(tuple, args) * kwargs.get('repeat', 1) + result = [[]] + for pool in pools: + result = [x + [y] for x in result for y in pool] + for prod in result: + yield tuple(prod) + + def zip_longest(*args, **kwds): + # zip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D- + fillvalue = kwds.get('fillvalue') + def sentinel(counter = ([fillvalue]*(len(args)-1)).pop): + yield counter() # yields the fillvalue, or raises IndexError + fillers = repeat(fillvalue) + iters = [chain(it, sentinel(), fillers) for it in args] + try: + for tup in izip(*iters): + yield tup + except IndexError: + pass -try: +# ------------------------------------------------------------------------------ +# Missing builtins and codecs in Python < 2.5 + +if sys.version_info >= (2, 5): + # Python >= 2.5 + base_exception = BaseException + next = next any = any all = all -except NameError: + +else: + # Python 2.4 + base_exception = Exception + + # this is on Python 2, where the method is called "next" (it is refactored + # to __next__ by 2to3, but in that case never executed) + def next(iterator): + return iterator.next() + def all(gen): for i in gen: if not i: @@ -124,8 +119,6 @@ except NameError: return True return False - -if sys.version_info < (2, 5): # Python 2.4 doesn't know the utf-8-sig encoding, so deliver it here def my_search_function(encoding): -- cgit v1.2.1 From 5a02e9343f9456f0b7b16c81aa28405e227ab54b Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 22:14:57 +0200 Subject: Invert setup.py uuid logic. --- setup.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index af9e660c..3b1d2d91 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ A development egg can be found `here <http://bitbucket.org/birkenfeld/sphinx/get/tip.gz#egg=Sphinx-dev>`_. ''' -requires = ['Pygments>=0.8', 'Jinja2>=2.2', 'docutils>=0.5', 'uuid>=1.30'] +requires = ['Pygments>=0.8', 'Jinja2>=2.2', 'docutils>=0.5'] if sys.version_info < (2, 4): print('ERROR: Sphinx requires at least Python 2.4 to run.') @@ -61,10 +61,10 @@ if sys.version_info < (2, 5): except: pass else: - del requires[-2] -elif sys.version_info >= (2, 5): - # An uuid module has been added to the stdlib in 2.5 - del requires[-1] + del requires[-1] + + # The uuid module is new in the stdlib in 2.5 + requires.append('uuid>=1.30') # Provide a "compile_catalog" command that also creates the translated -- cgit v1.2.1 From 14e4af35417585392628ceddcbb5402278b4a2ca Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 22:19:50 +0200 Subject: Next is new in 2.6. --- sphinx/util/pycompat.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index e5a712c9..319312a7 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -50,10 +50,12 @@ else: # ------------------------------------------------------------------------------ -# Missing itertools in Python < 2.6 +# Missing builtins and itertools in Python < 2.6 if sys.version_info >= (2, 6): # Python >= 2.6 + next = next + from itertools import product try: from itertools import zip_longest # Python 3 name @@ -64,6 +66,11 @@ else: # Python < 2.6 from itertools import izip, repeat, chain + # this is on Python 2, where the method is called "next" (it is refactored + # to __next__ by 2to3, but in that case never executed) + def next(iterator): + return iterator.next() + # These replacement functions have been taken from the Python 2.6 # itertools documentation. def product(*args, **kwargs): @@ -94,7 +101,6 @@ else: if sys.version_info >= (2, 5): # Python >= 2.5 base_exception = BaseException - next = next any = any all = all @@ -102,11 +108,6 @@ else: # Python 2.4 base_exception = Exception - # this is on Python 2, where the method is called "next" (it is refactored - # to __next__ by 2to3, but in that case never executed) - def next(iterator): - return iterator.next() - def all(gen): for i in gen: if not i: -- cgit v1.2.1 From 86fce1ff9d13f8d661c7c67fb5e3626b9d11252d Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 22:28:44 +0200 Subject: Fail early with old sqlalchemy. --- sphinx/websupport/storage/sqlalchemystorage.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sphinx/websupport/storage/sqlalchemystorage.py b/sphinx/websupport/storage/sqlalchemystorage.py index ba011c06..baef2481 100644 --- a/sphinx/websupport/storage/sqlalchemystorage.py +++ b/sphinx/websupport/storage/sqlalchemystorage.py @@ -11,9 +11,14 @@ from datetime import datetime +import sqlalchemy from sqlalchemy.orm import aliased from sqlalchemy.sql import func +if sqlalchemy.__version__[:3] < '0.5': + raise ImportError('SQLAlchemy version 0.5 or greater is required for this ' + 'storage backend; you have version %s' % sqlalchemy.__version__) + from sphinx.websupport.errors import CommentNotAllowedError, \ UserNotAuthorizedError from sphinx.websupport.storage import StorageBackend -- cgit v1.2.1 From 70e440f7c9fd041a51087abfc4dd7efdfe74b916 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 22:33:09 +0200 Subject: Move default sqlalchemy engine creation to storage backend. --- sphinx/websupport/__init__.py | 12 ++++++------ sphinx/websupport/storage/sqlalchemystorage.py | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sphinx/websupport/__init__.py b/sphinx/websupport/__init__.py index f2d2ba45..30303132 100644 --- a/sphinx/websupport/__init__.py +++ b/sphinx/websupport/__init__.py @@ -65,12 +65,12 @@ class WebSupport(object): # SQLAlchemy backend. from sphinx.websupport.storage.sqlalchemystorage \ import SQLAlchemyStorage - from sqlalchemy import create_engine - db_path = path.join(self.datadir, 'db', 'websupport.db') - ensuredir(path.dirname(db_path)) - uri = storage or 'sqlite:///%s' % db_path - engine = create_engine(uri) - self.storage = SQLAlchemyStorage(engine) + if not storage: + # no explicit DB path given; create default sqlite database + db_path = path.join(self.datadir, 'db', 'websupport.db') + ensuredir(path.dirname(db_path)) + storage = 'sqlite:///' + db_path + self.storage = SQLAlchemyStorage(storage) def _init_templating(self): import sphinx diff --git a/sphinx/websupport/storage/sqlalchemystorage.py b/sphinx/websupport/storage/sqlalchemystorage.py index baef2481..dea2eea0 100644 --- a/sphinx/websupport/storage/sqlalchemystorage.py +++ b/sphinx/websupport/storage/sqlalchemystorage.py @@ -32,11 +32,11 @@ class SQLAlchemyStorage(StorageBackend): A :class:`.StorageBackend` using SQLAlchemy. """ - def __init__(self, engine): - self.engine = engine - Base.metadata.bind = engine + def __init__(self, uri): + self.engine = sqlalchemy.create_engine(uri) + Base.metadata.bind = self.engine Base.metadata.create_all() - Session.configure(bind=engine) + Session.configure(bind=self.engine) def pre_build(self): self.build_session = Session() -- cgit v1.2.1 From e2dd1506291567ae962745acad1e50fd7c6835d1 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 22:36:35 +0200 Subject: Rename module to make clear it is only for sqlalchemy. --- sphinx/websupport/storage/db.py | 205 ------------------------- sphinx/websupport/storage/sqlalchemy_db.py | 205 +++++++++++++++++++++++++ sphinx/websupport/storage/sqlalchemystorage.py | 4 +- tests/test_websupport.py | 2 +- 4 files changed, 208 insertions(+), 208 deletions(-) delete mode 100644 sphinx/websupport/storage/db.py create mode 100644 sphinx/websupport/storage/sqlalchemy_db.py diff --git a/sphinx/websupport/storage/db.py b/sphinx/websupport/storage/db.py deleted file mode 100644 index bf7b83df..00000000 --- a/sphinx/websupport/storage/db.py +++ /dev/null @@ -1,205 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.websupport.storage.db - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - SQLAlchemy table and mapper definitions used by the - :class:`sphinx.websupport.comments.SQLAlchemyStorage`. - - :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -""" - -from datetime import datetime - -from sqlalchemy import Column, Integer, Text, String, Boolean, \ - ForeignKey, DateTime -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import relation, sessionmaker, aliased - -Base = declarative_base() - -Session = sessionmaker() - -db_prefix = 'sphinx_' - -class Node(Base): - """Data about a Node in a doctree.""" - __tablename__ = db_prefix + 'nodes' - - id = Column(String(32), primary_key=True) - document = Column(String(256), nullable=False) - source = Column(Text, nullable=False) - - def nested_comments(self, username, moderator): - """Create a tree of comments. First get all comments that are - descendents of this node, then convert them to a tree form. - - :param username: the name of the user to get comments for. - :param moderator: whether the user is moderator. - """ - session = Session() - - if username: - # If a username is provided, create a subquery to retrieve all - # votes by this user. We will outerjoin with the comment query - # with this subquery so we have a user's voting information. - sq = session.query(CommentVote).\ - filter(CommentVote.username == username).subquery() - cvalias = aliased(CommentVote, sq) - q = session.query(Comment, cvalias.value).outerjoin(cvalias) - else: - # If a username is not provided, we don't need to join with - # CommentVote. - q = session.query(Comment) - - # Filter out all comments not descending from this node. - q = q.filter(Comment.path.like(str(self.id) + '.%')) - - if not moderator: - q = q.filter(Comment.displayed == True) - - # Retrieve all results. Results must be ordered by Comment.path - # so that we can easily transform them from a flat list to a tree. - results = q.order_by(Comment.path).all() - session.close() - - return self._nest_comments(results, username) - - def _nest_comments(self, results, username): - """Given the flat list of results, convert the list into a - tree. - - :param results: the flat list of comments - :param username: the name of the user requesting the comments. - """ - comments = [] - list_stack = [comments] - for r in results: - comment, vote = r if username else (r, 0) - - inheritance_chain = comment.path.split('.')[1:] - - if len(inheritance_chain) == len(list_stack) + 1: - parent = list_stack[-1][-1] - list_stack.append(parent['children']) - elif len(inheritance_chain) < len(list_stack): - while len(inheritance_chain) < len(list_stack): - list_stack.pop() - - list_stack[-1].append(comment.serializable(vote=vote)) - - return comments - - def __init__(self, id, document, source): - self.id = id - self.document = document - self.source = source - - -class Comment(Base): - """An individual Comment being stored.""" - __tablename__ = db_prefix + 'comments' - - id = Column(Integer, primary_key=True) - rating = Column(Integer, nullable=False) - time = Column(DateTime, nullable=False) - text = Column(Text, nullable=False) - displayed = Column(Boolean, index=True, default=False) - username = Column(String(64)) - proposal = Column(Text) - proposal_diff = Column(Text) - path = Column(String(256), index=True) - - node_id = Column(String, ForeignKey(db_prefix + 'nodes.id')) - node = relation(Node, backref="comments") - - def __init__(self, text, displayed, username, rating, time, - proposal, proposal_diff): - self.text = text - self.displayed = displayed - self.username = username - self.rating = rating - self.time = time - self.proposal = proposal - self.proposal_diff = proposal_diff - - def set_path(self, node_id, parent_id): - """Set the materialized path for this comment.""" - # This exists because the path can't be set until the session has - # been flushed and this Comment has an id. - if node_id: - self.node_id = node_id - self.path = '%s.%s' % (node_id, self.id) - else: - session = Session() - parent_path = session.query(Comment.path).\ - filter(Comment.id == parent_id).one().path - session.close() - self.node_id = parent_path.split('.')[0] - self.path = '%s.%s' % (parent_path, self.id) - - def serializable(self, vote=0): - """Creates a serializable representation of the comment. This is - converted to JSON, and used on the client side. - """ - delta = datetime.now() - self.time - - time = {'year': self.time.year, - 'month': self.time.month, - 'day': self.time.day, - 'hour': self.time.hour, - 'minute': self.time.minute, - 'second': self.time.second, - 'iso': self.time.isoformat(), - 'delta': self.pretty_delta(delta)} - - path = self.path.split('.') - node = path[0] if len(path) == 2 else None - parent = path[-2] if len(path) > 2 else None - - return {'text': self.text, - 'username': self.username or 'Anonymous', - 'id': self.id, - 'node': node, - 'parent': parent, - 'rating': self.rating, - 'displayed': self.displayed, - 'age': delta.seconds, - 'time': time, - 'vote': vote or 0, - 'proposal_diff': self.proposal_diff, - 'children': []} - - def pretty_delta(self, delta): - """Create a pretty representation of the Comment's age. - (e.g. 2 minutes). - """ - days = delta.days - seconds = delta.seconds - hours = seconds / 3600 - minutes = seconds / 60 - - if days == 0: - dt = (minutes, 'minute') if hours == 0 else (hours, 'hour') - else: - dt = (days, 'day') - - return '%s %s ago' % dt if dt[0] == 1 else '%s %ss ago' % dt - - -class CommentVote(Base): - """A vote a user has made on a Comment.""" - __tablename__ = db_prefix + 'commentvote' - - username = Column(String(64), primary_key=True) - comment_id = Column(Integer, ForeignKey(db_prefix + 'comments.id'), - primary_key=True) - comment = relation(Comment, backref="votes") - # -1 if downvoted, +1 if upvoted, 0 if voted then unvoted. - value = Column(Integer, nullable=False) - - def __init__(self, comment_id, username, value): - self.comment_id = comment_id - self.username = username - self.value = value diff --git a/sphinx/websupport/storage/sqlalchemy_db.py b/sphinx/websupport/storage/sqlalchemy_db.py new file mode 100644 index 00000000..4e2757a9 --- /dev/null +++ b/sphinx/websupport/storage/sqlalchemy_db.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +""" + sphinx.websupport.storage.sqlalchemy_db + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + SQLAlchemy table and mapper definitions used by the + :class:`sphinx.websupport.storage.sqlalchemystorage.SQLAlchemyStorage`. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from datetime import datetime + +from sqlalchemy import Column, Integer, Text, String, Boolean, \ + ForeignKey, DateTime +from sqlalchemy.orm import relation, sessionmaker, aliased +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() +Session = sessionmaker() + +db_prefix = 'sphinx_' + + +class Node(Base): + """Data about a Node in a doctree.""" + __tablename__ = db_prefix + 'nodes' + + id = Column(String(32), primary_key=True) + document = Column(String(256), nullable=False) + source = Column(Text, nullable=False) + + def nested_comments(self, username, moderator): + """Create a tree of comments. First get all comments that are + descendents of this node, then convert them to a tree form. + + :param username: the name of the user to get comments for. + :param moderator: whether the user is moderator. + """ + session = Session() + + if username: + # If a username is provided, create a subquery to retrieve all + # votes by this user. We will outerjoin with the comment query + # with this subquery so we have a user's voting information. + sq = session.query(CommentVote).\ + filter(CommentVote.username == username).subquery() + cvalias = aliased(CommentVote, sq) + q = session.query(Comment, cvalias.value).outerjoin(cvalias) + else: + # If a username is not provided, we don't need to join with + # CommentVote. + q = session.query(Comment) + + # Filter out all comments not descending from this node. + q = q.filter(Comment.path.like(str(self.id) + '.%')) + + if not moderator: + q = q.filter(Comment.displayed == True) + + # Retrieve all results. Results must be ordered by Comment.path + # so that we can easily transform them from a flat list to a tree. + results = q.order_by(Comment.path).all() + session.close() + + return self._nest_comments(results, username) + + def _nest_comments(self, results, username): + """Given the flat list of results, convert the list into a + tree. + + :param results: the flat list of comments + :param username: the name of the user requesting the comments. + """ + comments = [] + list_stack = [comments] + for r in results: + comment, vote = r if username else (r, 0) + + inheritance_chain = comment.path.split('.')[1:] + + if len(inheritance_chain) == len(list_stack) + 1: + parent = list_stack[-1][-1] + list_stack.append(parent['children']) + elif len(inheritance_chain) < len(list_stack): + while len(inheritance_chain) < len(list_stack): + list_stack.pop() + + list_stack[-1].append(comment.serializable(vote=vote)) + + return comments + + def __init__(self, id, document, source): + self.id = id + self.document = document + self.source = source + + +class Comment(Base): + """An individual Comment being stored.""" + __tablename__ = db_prefix + 'comments' + + id = Column(Integer, primary_key=True) + rating = Column(Integer, nullable=False) + time = Column(DateTime, nullable=False) + text = Column(Text, nullable=False) + displayed = Column(Boolean, index=True, default=False) + username = Column(String(64)) + proposal = Column(Text) + proposal_diff = Column(Text) + path = Column(String(256), index=True) + + node_id = Column(String, ForeignKey(db_prefix + 'nodes.id')) + node = relation(Node, backref="comments") + + def __init__(self, text, displayed, username, rating, time, + proposal, proposal_diff): + self.text = text + self.displayed = displayed + self.username = username + self.rating = rating + self.time = time + self.proposal = proposal + self.proposal_diff = proposal_diff + + def set_path(self, node_id, parent_id): + """Set the materialized path for this comment.""" + # This exists because the path can't be set until the session has + # been flushed and this Comment has an id. + if node_id: + self.node_id = node_id + self.path = '%s.%s' % (node_id, self.id) + else: + session = Session() + parent_path = session.query(Comment.path).\ + filter(Comment.id == parent_id).one().path + session.close() + self.node_id = parent_path.split('.')[0] + self.path = '%s.%s' % (parent_path, self.id) + + def serializable(self, vote=0): + """Creates a serializable representation of the comment. This is + converted to JSON, and used on the client side. + """ + delta = datetime.now() - self.time + + time = {'year': self.time.year, + 'month': self.time.month, + 'day': self.time.day, + 'hour': self.time.hour, + 'minute': self.time.minute, + 'second': self.time.second, + 'iso': self.time.isoformat(), + 'delta': self.pretty_delta(delta)} + + path = self.path.split('.') + node = path[0] if len(path) == 2 else None + parent = path[-2] if len(path) > 2 else None + + return {'text': self.text, + 'username': self.username or 'Anonymous', + 'id': self.id, + 'node': node, + 'parent': parent, + 'rating': self.rating, + 'displayed': self.displayed, + 'age': delta.seconds, + 'time': time, + 'vote': vote or 0, + 'proposal_diff': self.proposal_diff, + 'children': []} + + def pretty_delta(self, delta): + """Create a pretty representation of the Comment's age. + (e.g. 2 minutes). + """ + days = delta.days + seconds = delta.seconds + hours = seconds / 3600 + minutes = seconds / 60 + + if days == 0: + dt = (minutes, 'minute') if hours == 0 else (hours, 'hour') + else: + dt = (days, 'day') + + return '%s %s ago' % dt if dt[0] == 1 else '%s %ss ago' % dt + + +class CommentVote(Base): + """A vote a user has made on a Comment.""" + __tablename__ = db_prefix + 'commentvote' + + username = Column(String(64), primary_key=True) + comment_id = Column(Integer, ForeignKey(db_prefix + 'comments.id'), + primary_key=True) + comment = relation(Comment, backref="votes") + # -1 if downvoted, +1 if upvoted, 0 if voted then unvoted. + value = Column(Integer, nullable=False) + + def __init__(self, comment_id, username, value): + self.comment_id = comment_id + self.username = username + self.value = value diff --git a/sphinx/websupport/storage/sqlalchemystorage.py b/sphinx/websupport/storage/sqlalchemystorage.py index dea2eea0..6f13c91b 100644 --- a/sphinx/websupport/storage/sqlalchemystorage.py +++ b/sphinx/websupport/storage/sqlalchemystorage.py @@ -22,8 +22,8 @@ if sqlalchemy.__version__[:3] < '0.5': from sphinx.websupport.errors import CommentNotAllowedError, \ UserNotAuthorizedError from sphinx.websupport.storage import StorageBackend -from sphinx.websupport.storage.db import Base, Node, Comment, \ - CommentVote, Session +from sphinx.websupport.storage.sqlalchemy_db import Base, Node, \ + Comment, CommentVote, Session from sphinx.websupport.storage.differ import CombinedHtmlDiff diff --git a/tests/test_websupport.py b/tests/test_websupport.py index bf66cb3b..e008556c 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -25,7 +25,7 @@ from sphinx.websupport.errors import * from sphinx.websupport.storage.differ import CombinedHtmlDiff from sphinx.websupport.storage.sqlalchemystorage import Session, \ SQLAlchemyStorage, Comment, CommentVote -from sphinx.websupport.storage.db import Node +from sphinx.websupport.storage.sqlalchemy_db import Node from util import * -- cgit v1.2.1 From 25ff50b600135e3a0bd2ba4faeeb886d1d55fb6a Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 22:47:18 +0200 Subject: Add skip_if and skip_unless test decorators. --- tests/util.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/util.py b/tests/util.py index b81e15b6..cb1a980a 100644 --- a/tests/util.py +++ b/tests/util.py @@ -25,12 +25,12 @@ from sphinx.ext.autodoc import AutoDirective from path import path -from nose import tools +from nose import tools, SkipTest __all__ = [ 'test_root', - 'raises', 'raises_msg', 'Struct', + 'raises', 'raises_msg', 'skip_if', 'skip_unless', 'Struct', 'ListOutput', 'TestApp', 'with_app', 'gen_with_app', 'path', 'with_tempdir', 'write_file', 'sprint', 'remove_unicode_literals', @@ -71,6 +71,21 @@ def raises_msg(exc, msg, func, *args, **kwds): raise AssertionError('%s did not raise %s' % (func.__name__, _excstr(exc))) +def skip_if(condition, msg=None): + """Decorator to skip test if condition is true.""" + def deco(test): + @tools.make_decorator(test) + def skipper(*args, **kwds): + if condition: + raise SkipTest(msg or 'conditional skip') + return test(*args, **kwds) + return skipper + return deco + +def skip_unless(condition, msg=None): + """Decorator to skip test if condition is false.""" + return skip_if(not condition, msg) + class Struct(object): def __init__(self, **kwds): -- cgit v1.2.1 From 73e9cebc68f2e60193b6f3f685eb1245bd7825a0 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 22:47:32 +0200 Subject: Skip tests accordingly if sqlalchemy is not present. --- tests/test_websupport.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/test_websupport.py b/tests/test_websupport.py index e008556c..1ac96f8d 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -23,9 +23,13 @@ from nose import SkipTest from sphinx.websupport import WebSupport from sphinx.websupport.errors import * from sphinx.websupport.storage.differ import CombinedHtmlDiff -from sphinx.websupport.storage.sqlalchemystorage import Session, \ - SQLAlchemyStorage, Comment, CommentVote -from sphinx.websupport.storage.sqlalchemy_db import Node +try: + from sphinx.websupport.storage.sqlalchemystorage import Session, \ + SQLAlchemyStorage, Comment, CommentVote + from sphinx.websupport.storage.sqlalchemy_db import Node + sqlalchemy_missing = False +except ImportError: + sqlalchemy_missing = True from util import * @@ -73,6 +77,7 @@ def test_get_document(support): and contents['sidebar'] and contents['relbar'] +@skip_if(sqlalchemy_missing, 'needs sqlalchemy') @with_support() def test_comments(support): session = Session() @@ -121,6 +126,7 @@ def test_comments(support): assert children[0]['text'] == 'Child test comment' +@skip_if(sqlalchemy_missing, 'needs sqlalchemy') @with_support() def test_voting(support): session = Session() @@ -154,6 +160,7 @@ def test_voting(support): assert comment['vote'] == 1, '%s != 1' % comment['vote'] +@skip_if(sqlalchemy_missing, 'needs sqlalchemy') @with_support() def test_proposals(support): session = Session() @@ -169,6 +176,7 @@ def test_proposals(support): proposal=proposal) +@skip_if(sqlalchemy_missing, 'needs sqlalchemy') @with_support() def test_user_delete_comments(support): def get_comment(): @@ -189,6 +197,7 @@ def test_user_delete_comments(support): assert comment['text'] == '[deleted]' +@skip_if(sqlalchemy_missing, 'needs sqlalchemy') @with_support() def test_moderator_delete_comments(support): def get_comment(): @@ -205,6 +214,7 @@ def test_moderator_delete_comments(support): assert comment['text'] == '[deleted]' +@skip_if(sqlalchemy_missing, 'needs sqlalchemy') @with_support() def test_update_username(support): support.update_username('user_two', 'new_user_two') @@ -229,6 +239,7 @@ def moderation_callback(comment): called = True +@skip_if(sqlalchemy_missing, 'needs sqlalchemy') @with_support(moderation_callback=moderation_callback) def test_moderation(support): session = Session() -- cgit v1.2.1 From 4ead60c306775e951aff469cc126309aaffc3520 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 23:03:06 +0200 Subject: Improve websupport test skipping, add new decorator for search adapter skipping. --- tests/test_searchadapters.py | 26 +++++++++++--------------- tests/test_websupport.py | 9 ++++++++- tests/util.py | 13 +++++++++++-- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/tests/test_searchadapters.py b/tests/test_searchadapters.py index a30141df..cf5accb9 100644 --- a/tests/test_searchadapters.py +++ b/tests/test_searchadapters.py @@ -12,9 +12,13 @@ import os, sys from StringIO import StringIO -from util import * +from nose import SkipTest + from sphinx.websupport import WebSupport +from test_websupport import sqlalchemy_missing +from util import * + def clear_builddir(): (test_root / 'websupport').rmtree(True) @@ -63,21 +67,13 @@ def search_adapter_helper(adapter): html = support.get_search_results(u'SomeLongRandomWord') +@skip_unless_importable('xapian', 'needs xapian bindings installed') +@skip_if(sqlalchemy_missing, 'needs sqlalchemy') def test_xapian(): - # Don't run tests if xapian is not installed. - try: - import xapian - search_adapter_helper('xapian') - except ImportError: - sys.stderr.write('info: not running xapian tests, ' \ - 'xapian doesn\'t seem to be installed') + search_adapter_helper('xapian') +@skip_unless_importable('whoosh', 'needs whoosh package installed') +@skip_if(sqlalchemy_missing, 'needs sqlalchemy') def test_whoosh(): - # Don't run tests if whoosh is not installed. - try: - import whoosh - search_adapter_helper('whoosh') - except ImportError: - sys.stderr.write('info: not running whoosh tests, ' \ - 'whoosh doesn\'t seem to be installed') + search_adapter_helper('whoosh') diff --git a/tests/test_websupport.py b/tests/test_websupport.py index 1ac96f8d..65957378 100644 --- a/tests/test_websupport.py +++ b/tests/test_websupport.py @@ -22,6 +22,7 @@ from nose import SkipTest from sphinx.websupport import WebSupport from sphinx.websupport.errors import * +from sphinx.websupport.storage import StorageBackend from sphinx.websupport.storage.differ import CombinedHtmlDiff try: from sphinx.websupport.storage.sqlalchemystorage import Session, \ @@ -57,17 +58,23 @@ def with_support(*args, **kwargs): return generator -@with_support() +class NullStorage(StorageBackend): + pass + + +@with_support(storage=NullStorage()) def test_no_srcdir(support): """Make sure the correct exception is raised if srcdir is not given.""" raises(SrcdirNotSpecifiedError, support.build) +@skip_if(sqlalchemy_missing, 'needs sqlalchemy') @with_support(srcdir=test_root) def test_build(support): support.build() +@skip_if(sqlalchemy_missing, 'needs sqlalchemy') @with_support() def test_get_document(support): raises(DocumentNotFoundError, support.get_document, 'nonexisting') diff --git a/tests/util.py b/tests/util.py index cb1a980a..d56f3464 100644 --- a/tests/util.py +++ b/tests/util.py @@ -29,8 +29,8 @@ from nose import tools, SkipTest __all__ = [ - 'test_root', - 'raises', 'raises_msg', 'skip_if', 'skip_unless', 'Struct', + 'test_root', 'raises', 'raises_msg', + 'skip_if', 'skip_unless', 'skip_unless_importable', 'Struct', 'ListOutput', 'TestApp', 'with_app', 'gen_with_app', 'path', 'with_tempdir', 'write_file', 'sprint', 'remove_unicode_literals', @@ -86,6 +86,15 @@ def skip_unless(condition, msg=None): """Decorator to skip test if condition is false.""" return skip_if(not condition, msg) +def skip_unless_importable(module, msg=None): + """Decorator to skip test if module is not importable.""" + try: + __import__(module) + except ImportError: + return skip_if(True, msg) + else: + return skip_if(False, msg) + class Struct(object): def __init__(self, **kwds): -- cgit v1.2.1 From 7b23361e75f65e6bb2dd566bb9ca840c343d541c Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 23:45:25 +0200 Subject: Remove debugging leftover. --- sphinx/ext/oldcmarkup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sphinx/ext/oldcmarkup.py b/sphinx/ext/oldcmarkup.py index 571d82a5..00ac3749 100644 --- a/sphinx/ext/oldcmarkup.py +++ b/sphinx/ext/oldcmarkup.py @@ -31,7 +31,6 @@ class OldCDirective(Directive): def run(self): env = self.state.document.settings.env if not env.app._oldcmarkup_warned: - print 'XXXYYY' env.warn(env.docname, WARNING_MSG, self.lineno) env.app._oldcmarkup_warned = True newname = 'c:' + self.name[1:] -- cgit v1.2.1 From 8824e63b058cd57532d55ee6df6438af1fc88546 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sat, 21 Aug 2010 23:59:40 +0200 Subject: Use different Whoosh API in order to fix test failure. --- sphinx/websupport/search/whooshsearch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sphinx/websupport/search/whooshsearch.py b/sphinx/websupport/search/whooshsearch.py index d395dcd7..e58c7342 100644 --- a/sphinx/websupport/search/whooshsearch.py +++ b/sphinx/websupport/search/whooshsearch.py @@ -11,6 +11,7 @@ from whoosh import index from whoosh.fields import Schema, ID, TEXT +from whoosh.qparser import QueryParser from whoosh.analysis import StemmingAnalyzer from sphinx.util.osutil import ensuredir @@ -31,6 +32,7 @@ class WhooshSearch(BaseSearch): self.index = index.open_dir(db_path) else: self.index = index.create_in(db_path, schema=self.schema) + self.qparser = QueryParser('text', self.schema) def init_indexing(self, changed=[]): for changed_path in changed: @@ -47,7 +49,7 @@ class WhooshSearch(BaseSearch): def handle_query(self, q): searcher = self.index.searcher() - whoosh_results = searcher.find('text', q) + whoosh_results = searcher.search(self.qparser.parse(q)) results = [] for result in whoosh_results: context = self.extract_context(result['text']) -- cgit v1.2.1 From 9042a5893e117e323f3758a1da009788a3580ed5 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sun, 22 Aug 2010 00:32:09 +0200 Subject: Remove GSOC changelogs. --- CHANGES.DasIch | 36 ------------------------------------ CHANGES.jacobmason | 27 --------------------------- 2 files changed, 63 deletions(-) delete mode 100644 CHANGES.DasIch delete mode 100644 CHANGES.jacobmason diff --git a/CHANGES.DasIch b/CHANGES.DasIch deleted file mode 100644 index 3f716726..00000000 --- a/CHANGES.DasIch +++ /dev/null @@ -1,36 +0,0 @@ -Changes -======= - -This file contains changes made by Daniel Neuhäuser, during the Google Summer -of Code 2010, to port Sphinx to Python 3.x. Changes are ordered descending by -date. - -May 16: - Added utils/convert.py which converts entire directories of python - files with 2to3 and names the converted files foo3.py. - - Modified the Makefile so that in case Python 3 is used the scripts in - utils get converted with utils/convert.py and are used instead of the - Python 2 scripts. - -May 10: Fixed a couple of tests and made several small changes. - -May 9: - Removed ez_setup.py which does not work with Python 3.x. and replaced - it with distribute_setup.py - - Use distribute (at least on 3.x) in order to run 2to3 automatically. - - Reverted some of the changes made in revision bac40c7c924c which - caused errors. - - Modified tests/run.py to test against the build created by - setup.py build in order to run the test suite with 3.x - - Several small changes to fix 3.x compatibilty. - -May 1: - Removed deprecated tuple parameter unpacking. - - Removed a pre-2.3 workaround for booleans because this creates a - deprecation warning for 3.x, in which you can't assign values to - booleans. - - Moved :func:`open()` calls out of the try-blocks, which fixes revision - c577c25bd44b. - -April 30: Made :cls:`sphinx.domains.cpp.DefExpr` unhashable as described by the - documentation because classes in 3.x don't inherit ``__hash__`` if - they implement ``__eq__``. - -April 29: Removed several deprecated function/method calls. diff --git a/CHANGES.jacobmason b/CHANGES.jacobmason deleted file mode 100644 index c445006c..00000000 --- a/CHANGES.jacobmason +++ /dev/null @@ -1,27 +0,0 @@ -May 30: Added files builders/websupport.py, writers/websupport.py, -websupport/api.py, and websupport/document.api. Provides a rudimentary -method of building websupport data, and rendering it as html. - -May 31-June 10: Continued changing way web support data is represented -and accessed. - -June 14 - June 17: Continued making improvements to the web support package -and demo web application. Included sidebars, navlinks etc... - -June 21 - June 26: Implement server side search with two search adapters, -one for Xapian and one for Whoosh - -June 28 - July 12: Implement voting system on the backend, and created a -jQuery script to handle voting on the frontend. - -July 13 - July 19: Added documentation for the web support package. - -July 20 - July 27: Added a system to allow user's to propose changes to -documentation along with comments. - -July 28 - August 3: Added tests for the web support package. Refactored -sqlalchemy storage to be more efficient. - -August 4 - August 7: Added comment moderation system. Added more -documentation. General code cleanup. - -- cgit v1.2.1 From f160503120e0f72ea8b70bb850c92b8023bb9f6e Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sun, 22 Aug 2010 11:09:35 +0200 Subject: Add node docstrings, remove duplication of class names. --- sphinx/addnodes.py | 195 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 130 insertions(+), 65 deletions(-) diff --git a/sphinx/addnodes.py b/sphinx/addnodes.py index 0a2f0f7f..592bef5d 100644 --- a/sphinx/addnodes.py +++ b/sphinx/addnodes.py @@ -11,103 +11,168 @@ from docutils import nodes -# index markup -class index(nodes.Invisible, nodes.Inline, nodes.TextElement): pass + +class toctree(nodes.General, nodes.Element): + """Node for inserting a "TOC tree".""" + # domain-specific object descriptions (class, function etc.) -# parent node for signature and content -class desc(nodes.Admonition, nodes.Element): pass +class desc(nodes.Admonition, nodes.Element): + """Node for object descriptions. + + This node is similar to a "definition list" with one definition. It + contains one or more ``desc_signature`` and a ``desc_content``. + """ + +class desc_signature(nodes.Part, nodes.Inline, nodes.TextElement): + """Node for object signatures. -# additional name parts (module name, class name) -class desc_addname(nodes.Part, nodes.Inline, nodes.TextElement): pass + The "term" part of the custom Sphinx definition list. + """ + + +# nodes to use within a desc_signature + +class desc_addname(nodes.Part, nodes.Inline, nodes.TextElement): + """Node for additional name parts (module name, class name).""" # compatibility alias desc_classname = desc_addname -# return type (C); object type -class desc_type(nodes.Part, nodes.Inline, nodes.TextElement): pass -# -> annotation (Python) + +class desc_type(nodes.Part, nodes.Inline, nodes.TextElement): + """Node for return types or object type names.""" + class desc_returns(desc_type): + """Node for a "returns" annotation (a la -> in Python).""" def astext(self): return ' -> ' + nodes.TextElement.astext(self) -# main name of object -class desc_name(nodes.Part, nodes.Inline, nodes.TextElement): pass -# argument list -class desc_signature(nodes.Part, nodes.Inline, nodes.TextElement): pass + +class desc_name(nodes.Part, nodes.Inline, nodes.TextElement): + """Node for the main object name.""" + class desc_parameterlist(nodes.Part, nodes.Inline, nodes.TextElement): + """Node for a general parameter list.""" child_text_separator = ', ' -class desc_parameter(nodes.Part, nodes.Inline, nodes.TextElement): pass + +class desc_parameter(nodes.Part, nodes.Inline, nodes.TextElement): + """Node for a single parameter.""" + class desc_optional(nodes.Part, nodes.Inline, nodes.TextElement): + """Node for marking optional parts of the parameter list.""" child_text_separator = ', ' def astext(self): return '[' + nodes.TextElement.astext(self) + ']' -# annotation (not Python 3-style annotations) -class desc_annotation(nodes.Part, nodes.Inline, nodes.TextElement): pass -# node for content -class desc_content(nodes.General, nodes.Element): pass +class desc_annotation(nodes.Part, nodes.Inline, nodes.TextElement): + """Node for signature annotations (not Python 3-style annotations).""" + +class desc_content(nodes.General, nodes.Element): + """Node for object description content. + + This is the "definition" part of the custom Sphinx definition list. + """ + + +# new admonition-like constructs + +class versionmodified(nodes.Admonition, nodes.TextElement): + """Node for version change entries. + + Currently used for "versionadded", "versionchanged" and "deprecated" + directives. + """ + +class seealso(nodes.Admonition, nodes.Element): + """Custom "see also" admonition.""" + +class productionlist(nodes.Admonition, nodes.Element): + """Node for grammar production lists. + + Contains ``production`` nodes. + """ + +class production(nodes.Part, nodes.Inline, nodes.TextElement): + """Node for a single grammar production rule.""" + + +# other directive-level nodes + +class index(nodes.Invisible, nodes.Inline, nodes.TextElement): + """Node for index entries. + + This node is created by the ``index`` directive and has one attribute, + ``entries``. Its value is a list of 4-tuples of ``(entrytype, entryname, + target, ignored)``. + + *entrytype* is one of "single", "pair", "double", "triple". + """ + +class centered(nodes.Part, nodes.Element): + """Deprecated.""" + +class acks(nodes.Element): + """Special node for "acks" lists.""" + +class hlist(nodes.Element): + """Node for "horizontal lists", i.e. lists that should be compressed to + take up less vertical space. + """ -# \versionadded, \versionchanged, \deprecated -class versionmodified(nodes.Admonition, nodes.TextElement): pass +class hlistcol(nodes.Element): + """Node for one column in a horizontal list.""" -# seealso -class seealso(nodes.Admonition, nodes.Element): pass +class compact_paragraph(nodes.paragraph): + """Node for a compact paragraph (which never makes a <p> node).""" -# productionlist -class productionlist(nodes.Admonition, nodes.Element): pass -class production(nodes.Part, nodes.Inline, nodes.TextElement): pass +class glossary(nodes.Element): + """Node to insert a glossary.""" -# toc tree -class toctree(nodes.General, nodes.Element): pass +class only(nodes.Element): + """Node for "only" directives (conditional inclusion based on tags).""" -# centered -class centered(nodes.Part, nodes.Element): pass -# pending xref -class pending_xref(nodes.Inline, nodes.Element): pass +# meta-information nodes -# compact paragraph -- never makes a <p> -class compact_paragraph(nodes.paragraph): pass +class start_of_file(nodes.Element): + """Node to mark start of a new file, used in the LaTeX builder only.""" -# reference to a file to download -class download_reference(nodes.reference): pass +class highlightlang(nodes.Element): + """Inserted to set the highlight language and line number options for + subsequent code blocks. + """ -# for the ACKS list -class acks(nodes.Element): pass +class tabular_col_spec(nodes.Element): + """Node for specifying tabular columns, used for LaTeX output.""" -# for horizontal lists -class hlist(nodes.Element): pass -class hlistcol(nodes.Element): pass +class meta(nodes.Special, nodes.PreBibliographic, nodes.Element): + """Node for meta directive -- same as docutils' standard meta node, + but pickleable. + """ -# sets the highlighting language for literal blocks -class highlightlang(nodes.Element): pass -# like emphasis, but doesn't apply further text processors, e.g. smartypants -class literal_emphasis(nodes.emphasis): pass +# inline nodes -# for abbreviations (with explanations) -class abbreviation(nodes.Inline, nodes.TextElement): pass +class pending_xref(nodes.Inline, nodes.Element): + """Node for cross-references that cannot be resolved without complete + information about all documents. -# glossary -class glossary(nodes.Element): pass + These nodes are resolved before writing output, in + BuildEnvironment.resolve_references. + """ -# start of a file, used in the LaTeX builder only -class start_of_file(nodes.Element): pass +class download_reference(nodes.reference): + """Node for download references, similar to pending_xref.""" -# tabular column specification, used for the LaTeX writer -class tabular_col_spec(nodes.Element): pass +class literal_emphasis(nodes.emphasis): + """Node that behaves like `emphasis`, but further text processors are not + applied (e.g. smartypants for HTML output). + """ -# only (in/exclusion based on tags) -class only(nodes.Element): pass +class abbreviation(nodes.Inline, nodes.TextElement): + """Node for abbreviations with explanations.""" -# meta directive -- same as docutils' standard meta node, but pickleable -class meta(nodes.Special, nodes.PreBibliographic, nodes.Element): pass -# make them known to docutils. this is needed, because the HTML writer -# will choke at some point if these are not added -nodes._add_node_class_names("""index desc desc_content desc_signature - desc_type desc_returns desc_addname desc_name desc_parameterlist - desc_parameter desc_optional download_reference hlist hlistcol - centered versionmodified seealso productionlist production toctree - pending_xref compact_paragraph highlightlang literal_emphasis - abbreviation glossary acks module start_of_file tabular_col_spec - meta""".split()) +# make the new nodes known to docutils; needed because the HTML writer will +# choke at some point if these are not added +nodes._add_node_class_names(k for k in globals().keys() + if k != 'nodes' and k[0] != '_') -- cgit v1.2.1 From 5978c17aed477cc2bbab7eae86bb8b18367c426e Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Sun, 22 Aug 2010 11:36:08 +0200 Subject: Docstring harmonization. --- sphinx/__init__.py | 1 + sphinx/application.py | 19 ++++------ sphinx/builders/__init__.py | 43 +++++++++++----------- sphinx/builders/devhelp.py | 1 - sphinx/builders/epub.py | 32 +++++++++-------- sphinx/builders/html.py | 3 +- sphinx/config.py | 4 ++- sphinx/domains/__init__.py | 29 +++++---------- sphinx/domains/cpp.py | 25 +++++++------ sphinx/domains/javascript.py | 2 +- sphinx/domains/python.py | 22 +++++------- sphinx/domains/rst.py | 7 ++-- sphinx/environment.py | 73 +++++++++++++++++--------------------- sphinx/ext/autodoc.py | 68 +++++++++++++++++------------------ sphinx/ext/autosummary/__init__.py | 22 +++++------- sphinx/ext/autosummary/generate.py | 13 +++---- sphinx/ext/graphviz.py | 4 +-- sphinx/ext/inheritance_diagram.py | 32 ++++++----------- sphinx/ext/oldcmarkup.py | 1 + sphinx/ext/pngmath.py | 3 +- sphinx/jinja2glue.py | 6 ++-- sphinx/locale/__init__.py | 11 +++--- sphinx/roles.py | 11 +++--- sphinx/setup_command.py | 3 +- sphinx/theming.py | 14 +++----- sphinx/util/__init__.py | 19 ++++------ sphinx/util/docstrings.py | 15 ++++---- sphinx/util/jsonimpl.py | 2 +- sphinx/util/matching.py | 11 +++--- sphinx/util/nodes.py | 6 ++++ sphinx/util/osutil.py | 4 +-- sphinx/util/png.py | 8 ++--- sphinx/util/websupport.py | 1 + sphinx/writers/websupport.py | 3 +- 34 files changed, 233 insertions(+), 285 deletions(-) diff --git a/sphinx/__init__.py b/sphinx/__init__.py index 1ea2e7bf..211e2413 100644 --- a/sphinx/__init__.py +++ b/sphinx/__init__.py @@ -37,6 +37,7 @@ if '+' in __version__ or 'pre' in __version__: def main(argv=sys.argv): + """Sphinx build "main" command-line entry.""" if sys.version_info[:3] < (2, 4, 0): sys.stderr.write('Error: Sphinx requires at least ' 'Python 2.4 to run.\n') diff --git a/sphinx/application.py b/sphinx/application.py index b3d2aebc..6fe1ee05 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -133,9 +133,8 @@ class Sphinx(object): self._init_builder(buildername) def _init_i18n(self): - """ - Load translated strings from the configured localedirs if - enabled in the configuration. + """Load translated strings from the configured localedirs if enabled in + the configuration. """ if self.config.language is not None: self.info(bold('loading translations [%s]... ' % @@ -484,8 +483,7 @@ class TemplateBridge(object): """ def init(self, builder, theme=None, dirs=None): - """ - Called by the builder to initialize the template system. + """Called by the builder to initialize the template system. *builder* is the builder object; you'll probably want to look at the value of ``builder.config.templates_path``. @@ -496,23 +494,20 @@ class TemplateBridge(object): raise NotImplementedError('must be implemented in subclasses') def newest_template_mtime(self): - """ - Called by the builder to determine if output files are outdated + """Called by the builder to determine if output files are outdated because of template changes. Return the mtime of the newest template file that was changed. The default implementation returns ``0``. """ return 0 def render(self, template, context): - """ - Called by the builder to render a template given as a filename with a - specified context (a Python dictionary). + """Called by the builder to render a template given as a filename with + a specified context (a Python dictionary). """ raise NotImplementedError('must be implemented in subclasses') def render_string(self, template, context): - """ - Called by the builder to render a template given as a string with a + """Called by the builder to render a template given as a string with a specified context (a Python dictionary). """ raise NotImplementedError('must be implemented in subclasses') diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 9112af63..ce04f769 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -55,16 +55,13 @@ class Builder(object): # helper methods def init(self): - """ - Load necessary templates and perform initialization. The default + """Load necessary templates and perform initialization. The default implementation does nothing. """ pass def create_template_bridge(self): - """ - Return the template bridge configured. - """ + """Return the template bridge configured.""" if self.config.template_bridge: self.templates = self.app.import_object( self.config.template_bridge, 'template_bridge setting')() @@ -73,23 +70,23 @@ class Builder(object): self.templates = BuiltinTemplateLoader() def get_target_uri(self, docname, typ=None): - """ - Return the target URI for a document name (*typ* can be used to qualify - the link characteristic for individual builders). + """Return the target URI for a document name. + + *typ* can be used to qualify the link characteristic for individual + builders. """ raise NotImplementedError def get_relative_uri(self, from_, to, typ=None): - """ - Return a relative URI between two source filenames. May raise - environment.NoUri if there's no way to return a sensible URI. + """Return a relative URI between two source filenames. + + May raise environment.NoUri if there's no way to return a sensible URI. """ return relative_uri(self.get_target_uri(from_), self.get_target_uri(to, typ)) def get_outdated_docs(self): - """ - Return an iterable of output files that are outdated, or a string + """Return an iterable of output files that are outdated, or a string describing what an update build will build. If the builder does not output individual files corresponding to @@ -129,9 +126,7 @@ class Builder(object): supported_image_types = [] def post_process_images(self, doctree): - """ - Pick the best candidate for all image URIs. - """ + """Pick the best candidate for all image URIs.""" for node in doctree.traverse(nodes.image): if '?' in node['candidates']: # don't rewrite nonlocal image URIs @@ -198,9 +193,9 @@ class Builder(object): 'out of date' % len(to_build)) def build(self, docnames, summary=None, method='update'): - """ - Main build method. First updates the environment, and then - calls :meth:`write`. + """Main build method. + + First updates the environment, and then calls :meth:`write`. """ if summary: self.info(bold('building [%s]: ' % self.name), nonl=1) @@ -302,14 +297,16 @@ class Builder(object): raise NotImplementedError def finish(self): - """ - Finish the building process. The default implementation does nothing. + """Finish the building process. + + The default implementation does nothing. """ pass def cleanup(self): - """ - Cleanup any resources. The default implementation does nothing. + """Cleanup any resources. + + The default implementation does nothing. """ pass diff --git a/sphinx/builders/devhelp.py b/sphinx/builders/devhelp.py index a5a0f280..d43cd624 100644 --- a/sphinx/builders/devhelp.py +++ b/sphinx/builders/devhelp.py @@ -42,7 +42,6 @@ except ImportError: class DevhelpBuilder(StandaloneHTMLBuilder): """ Builder that also outputs GNOME Devhelp file. - """ name = 'devhelp' diff --git a/sphinx/builders/epub.py b/sphinx/builders/epub.py index aea07d4d..3e123a0a 100644 --- a/sphinx/builders/epub.py +++ b/sphinx/builders/epub.py @@ -130,7 +130,8 @@ _refuri_re = re.compile("([^#:]*#)(.*)") # The epub publisher class EpubBuilder(StandaloneHTMLBuilder): - """Builder that outputs epub files. + """ + Builder that outputs epub files. It creates the metainfo files container.opf, toc.ncx, mimetype, and META-INF/container.xml. Afterwards, all necessary files are zipped to an @@ -222,12 +223,12 @@ class EpubBuilder(StandaloneHTMLBuilder): }) def fix_fragment(self, match): - """Return a href attribute with colons replaced by hyphens. - """ + """Return a href attribute with colons replaced by hyphens.""" return match.group(1) + match.group(2).replace(':', '-') def fix_ids(self, tree): """Replace colons with hyphens in href and id attributes. + Some readers crash because they interpret the part as a transport protocol specification. """ @@ -246,8 +247,7 @@ class EpubBuilder(StandaloneHTMLBuilder): node.attributes['ids'] = newids def add_visible_links(self, tree): - """Append visible link targets after external links. - """ + """Append visible link targets after external links.""" for node in tree.traverse(nodes.reference): uri = node.get('refuri', '') if (uri.startswith('http:') or uri.startswith('https:') or @@ -261,6 +261,7 @@ class EpubBuilder(StandaloneHTMLBuilder): def write_doc(self, docname, doctree): """Write one document file. + This method is overwritten in order to fix fragment identifiers and to add visible external links. """ @@ -269,8 +270,7 @@ class EpubBuilder(StandaloneHTMLBuilder): return StandaloneHTMLBuilder.write_doc(self, docname, doctree) def fix_genindex(self, tree): - """Fix href attributes for genindex pages. - """ + """Fix href attributes for genindex pages.""" # XXX: modifies tree inline # Logic modeled from themes/basic/genindex.html for key, columns in tree: @@ -288,8 +288,9 @@ class EpubBuilder(StandaloneHTMLBuilder): def handle_page(self, pagename, addctx, templatename='page.html', outfilename=None, event_arg=None): """Create a rendered page. - This method is overwritten for genindex pages in order to fix - href link attributes. + + This method is overwritten for genindex pages in order to fix href link + attributes. """ if pagename.startswith('genindex'): self.fix_genindex(addctx['genindexentries']) @@ -413,6 +414,7 @@ class EpubBuilder(StandaloneHTMLBuilder): def insert_subnav(self, node, subnav): """Insert nested navpoints for given node. + The node and subnav are already rendered to text. """ nlist = node.rsplit('\n', 1) @@ -422,8 +424,8 @@ class EpubBuilder(StandaloneHTMLBuilder): def build_navpoints(self, nodes): """Create the toc navigation structure. - Subelements of a node are nested inside the navpoint. - For nested nodes the parent node is reinserted in the subnav. + Subelements of a node are nested inside the navpoint. For nested nodes + the parent node is reinserted in the subnav. """ navstack = [] navlist = [] @@ -461,8 +463,8 @@ class EpubBuilder(StandaloneHTMLBuilder): return '\n'.join(navlist) def toc_metadata(self, level, navpoints): - """Create a dictionary with all metadata for the toc.ncx - file properly escaped. + """Create a dictionary with all metadata for the toc.ncx file + properly escaped. """ metadata = {} metadata['uid'] = self.config.epub_uid @@ -487,8 +489,8 @@ class EpubBuilder(StandaloneHTMLBuilder): def build_epub(self, outdir, outname): """Write the epub file. - It is a zip file with the mimetype file stored uncompressed - as the first entry. + It is a zip file with the mimetype file stored uncompressed as the first + entry. """ self.info('writing %s file...' % outname) projectfiles = ['META-INF/container.xml', 'content.opf', 'toc.ncx'] \ diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index 5a7d49cd..57330eb5 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -587,8 +587,7 @@ class StandaloneHTMLBuilder(Builder): self.theme.cleanup() def post_process_images(self, doctree): - """ - Pick the best candidate for an image and link down-scaled images to + """Pick the best candidate for an image and link down-scaled images to their high res version. """ Builder.post_process_images(self, doctree) diff --git a/sphinx/config.py b/sphinx/config.py index 6c27f85f..2e0a116c 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -25,7 +25,9 @@ if sys.version_info >= (3, 0): CONFIG_SYNTAX_ERROR += "\nDid you change the syntax from 2.x to 3.x?" class Config(object): - """Configuration file abstraction.""" + """ + Configuration file abstraction. + """ # the values are: (default, what needs to be rebuilt if changed) diff --git a/sphinx/domains/__init__.py b/sphinx/domains/__init__.py index d133a812..484cd968 100644 --- a/sphinx/domains/__init__.py +++ b/sphinx/domains/__init__.py @@ -66,9 +66,8 @@ class Index(object): self.domain = domain def generate(self, docnames=None): - """ - Return entries for the index given by *name*. If *docnames* is given, - restrict to entries referring to these docnames. + """Return entries for the index given by *name*. If *docnames* is + given, restrict to entries referring to these docnames. The return value is a tuple of ``(content, collapse)``, where *collapse* is a boolean that determines if sub-entries should start collapsed (for @@ -158,8 +157,7 @@ class Domain(object): self.objtypes_for_role = self._role2type.get def role(self, name): - """ - Return a role adapter function that always gives the registered + """Return a role adapter function that always gives the registered role its full name ('domain:name') as the first argument. """ if name in self._role_cache: @@ -175,8 +173,7 @@ class Domain(object): return role_adapter def directive(self, name): - """ - Return a directive adapter class that always gives the registered + """Return a directive adapter class that always gives the registered directive its full name ('domain:name') as ``self.name``. """ if name in self._directive_cache: @@ -195,21 +192,16 @@ class Domain(object): # methods that should be overwritten def clear_doc(self, docname): - """ - Remove traces of a document in the domain-specific inventories. - """ + """Remove traces of a document in the domain-specific inventories.""" pass def process_doc(self, env, docname, document): - """ - Process a document after it is read by the environment. - """ + """Process a document after it is read by the environment.""" pass def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): - """ - Resolve the ``pending_xref`` *node* with the given *typ* and *target*. + """Resolve the pending_xref *node* with the given *typ* and *target*. This method should return a new node, to replace the xref node, containing the *contnode* which is the markup content of the @@ -225,8 +217,7 @@ class Domain(object): pass def get_objects(self): - """ - Return an iterable of "object descriptions", which are tuples with + """Return an iterable of "object descriptions", which are tuples with five items: * `name` -- fully qualified name @@ -245,9 +236,7 @@ class Domain(object): return [] def get_type_name(self, type, primary=False): - """ - Return full name for given ObjType. - """ + """Return full name for given ObjType.""" if primary: return type.lname return _('%s %s') % (self.label, type.lname) diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 8df89459..a59b2b50 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -135,29 +135,31 @@ class DefExpr(object): __hash__ = None def clone(self): - """Close a definition expression node""" + """Clone a definition expression node.""" return deepcopy(self) def get_id(self): - """Returns the id for the node""" + """Return the id for the node.""" return u'' def get_name(self): - """Returns the name. Returns either `None` or a node with - a name you might call :meth:`split_owner` on. + """Return the name. + + Returns either `None` or a node with a name you might call + :meth:`split_owner` on. """ return None def split_owner(self): - """Nodes returned by :meth:`get_name` can split off their - owning parent. This function returns the owner and the - name as a tuple of two items. If a node does not support - it, :exc:`NotImplementedError` is raised. + """Nodes returned by :meth:`get_name` can split off their owning parent. + + This function returns the owner and the name as a tuple of two items. + If a node does not support it, :exc:`NotImplementedError` is raised. """ raise NotImplementedError() def prefix(self, prefix): - """Prefixes a name node (a node returned by :meth:`get_name`).""" + """Prefix a name node (a node returned by :meth:`get_name`).""" raise NotImplementedError() def __str__(self): @@ -984,8 +986,9 @@ class CPPFunctionObject(CPPObject): class CPPCurrentNamespace(Directive): - """This directive is just to tell Sphinx that we're documenting - stuff in namespace foo. + """ + This directive is just to tell Sphinx that we're documenting stuff in + namespace foo. """ has_content = False diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index 582e2adc..bc8e50f3 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -148,7 +148,7 @@ class JSCallable(JSObject): class JSConstructor(JSCallable): - """Like a callable but with a different prefix""" + """Like a callable but with a different prefix.""" display_prefix = 'class ' diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index cd87bfbd..77604dec 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -63,22 +63,21 @@ class PyObject(ObjectDescription): ] def get_signature_prefix(self, sig): - """ - May return a prefix to put before the object name in the signature. + """May return a prefix to put before the object name in the + signature. """ return '' def needs_arglist(self): - """ - May return true if an empty argument list is to be generated even if + """May return true if an empty argument list is to be generated even if the document contains none. """ return False def handle_signature(self, sig, signode): - """ - Transform a Python signature into RST nodes. - Returns (fully qualified name of the thing, classname if any). + """Transform a Python signature into RST nodes. + + Return (fully qualified name of the thing, classname if any). If inside a class, the current class name is handled intelligently: * it is stripped from the displayed name if present @@ -167,9 +166,7 @@ class PyObject(ObjectDescription): return fullname, name_prefix def get_index_text(self, modname, name): - """ - Return the text for the index entry of the object. - """ + """Return the text for the index entry of the object.""" raise NotImplementedError('must be implemented in subclasses') def add_target_and_index(self, name_cls, sig, signode): @@ -548,9 +545,8 @@ class PythonDomain(Domain): del self.data['modules'][modname] def find_obj(self, env, modname, classname, name, type, searchmode=0): - """ - Find a Python object for "name", perhaps using the given module and/or - classname. Returns a list of (name, object entry) tuples. + """Find a Python object for "name", perhaps using the given module + and/or classname. Returns a list of (name, object entry) tuples. """ # skip parens if name[-2:] == '()': diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py index d3ffc6bd..30134d9e 100644 --- a/sphinx/domains/rst.py +++ b/sphinx/domains/rst.py @@ -59,9 +59,10 @@ class ReSTMarkup(ObjectDescription): def parse_directive(d): - """ - Parses a directive signature. Returns (directive, arguments) string tuple. - if no arguments are given, returns (directive, ''). + """Parse a directive signature. + + Returns (directive, arguments) string tuple. If no arguments are given, + returns (directive, ''). """ dir = d.strip() if not dir.startswith('.'): diff --git a/sphinx/environment.py b/sphinx/environment.py index e9e984c3..f8d3641b 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -417,12 +417,12 @@ class BuildEnvironment: domain.clear_doc(docname) def doc2path(self, docname, base=True, suffix=None): - """ - Return the filename for the document name. - If base is True, return absolute path under self.srcdir. - If base is None, return relative path to self.srcdir. - If base is a path string, return absolute path under that. - If suffix is not None, add it instead of config.source_suffix. + """Return the filename for the document name. + + If *base* is True, return absolute path under self.srcdir. + If *base* is None, return relative path to self.srcdir. + If *base* is a path string, return absolute path under that. + If *suffix* is not None, add it instead of config.source_suffix. """ docname = docname.replace(SEP, path.sep) suffix = suffix or self.config.source_suffix @@ -434,8 +434,8 @@ class BuildEnvironment: return path.join(base, docname) + suffix def find_files(self, config): - """ - Find all source files in the source dir and put them in self.found_docs. + """Find all source files in the source dir and put them in + self.found_docs. """ matchers = compile_matchers( config.exclude_patterns[:] + @@ -448,9 +448,7 @@ class BuildEnvironment: self.srcdir, config.source_suffix, exclude_matchers=matchers)) def get_outdated_files(self, config_changed): - """ - Return (added, changed, removed) sets. - """ + """Return (added, changed, removed) sets.""" # clear all files no longer present removed = set(self.all_docs) - self.found_docs @@ -496,12 +494,12 @@ class BuildEnvironment: return added, changed, removed def update(self, config, srcdir, doctreedir, app=None): - """ - (Re-)read all files new or changed since last update. Returns a - summary, the total count of documents to reread and an iterator that - yields docnames as it processes them. Store all environment docnames in - the canonical format (ie using SEP as a separator in place of - os.path.sep). + """(Re-)read all files new or changed since last update. + + Returns a summary, the total count of documents to reread and an + iterator that yields docnames as it processes them. Store all + environment docnames in the canonical format (ie using SEP as a + separator in place of os.path.sep). """ config_changed = False if self.config is None: @@ -632,8 +630,8 @@ class BuildEnvironment: roles.role = role def read_doc(self, docname, src_path=None, save_parsed=True, app=None): - """ - Parse a file and add/update inventory entries for the doctree. + """Parse a file and add/update inventory entries for the doctree. + If srcpath is given, read from a different source file. """ # remove all inventory entries for that file @@ -785,9 +783,7 @@ class BuildEnvironment: # post-processing of read doctrees def filter_messages(self, doctree): - """ - Filter system messages from a doctree. - """ + """Filter system messages from a doctree.""" filterlevel = self.config.keep_warnings and 2 or 5 for node in doctree.traverse(nodes.system_message): if node['level'] < filterlevel: @@ -795,9 +791,7 @@ class BuildEnvironment: def process_dependencies(self, docname, doctree): - """ - Process docutils-generated dependency info. - """ + """Process docutils-generated dependency info.""" cwd = os.getcwd() frompath = path.join(path.normpath(self.srcdir), 'dummy') deps = doctree.settings.record_dependencies @@ -811,9 +805,7 @@ class BuildEnvironment: self.dependencies.setdefault(docname, set()).add(relpath) def process_downloads(self, docname, doctree): - """ - Process downloadable file paths. - """ + """Process downloadable file paths. """ docdir = path.dirname(self.doc2path(docname, base=None)) for node in doctree.traverse(addnodes.download_reference): targetname = node['reftarget'] @@ -831,9 +823,7 @@ class BuildEnvironment: node['filename'] = uniquename def process_images(self, docname, doctree): - """ - Process and rewrite image URIs. - """ + """Process and rewrite image URIs.""" docdir = path.dirname(self.doc2path(docname, base=None)) for node in doctree.traverse(nodes.image): # Map the mimetype to the corresponding image. The writer may @@ -888,8 +878,8 @@ class BuildEnvironment: self.images.add_file(docname, imgpath) def process_metadata(self, docname, doctree): - """ - Process the docinfo part of the doctree as metadata. + """Process the docinfo part of the doctree as metadata. + Keep processing minimal -- just return what docutils says. """ self.metadata[docname] = md = {} @@ -974,8 +964,7 @@ class BuildEnvironment: item.replace(para, compact_para) def create_title_from(self, docname, document): - """ - Add a title node to the document (just copy the first section title), + """Add a title node to the document (just copy the first section title), and store that title in the environment. """ titlenode = nodes.title() @@ -1013,7 +1002,8 @@ class BuildEnvironment: def note_toctree(self, docname, toctreenode): """Note a TOC tree directive in a document and gather information about - file relations from it.""" + file relations from it. + """ if toctreenode['glob']: self.glob_toctrees.add(docname) if toctreenode.get('numbered'): @@ -1119,7 +1109,9 @@ class BuildEnvironment: def get_domain(self, domainname): """Return the domain instance with the specified name. - Raises an ExtensionError if the domain is not registered.""" + + Raises an ExtensionError if the domain is not registered. + """ try: return self.domains[domainname] except KeyError: @@ -1144,7 +1136,8 @@ class BuildEnvironment: def get_and_resolve_doctree(self, docname, builder, doctree=None, prune_toctrees=True): """Read the doctree from the pickle, resolve cross-references and - toctrees and return it.""" + toctrees and return it. + """ if doctree is None: doctree = self.get_doctree(docname) @@ -1164,8 +1157,7 @@ class BuildEnvironment: def resolve_toctree(self, docname, builder, toctree, prune=True, maxdepth=0, titles_only=False, collapse=False, includehidden=False): - """ - Resolve a *toctree* node into individual bullet lists with titles + """Resolve a *toctree* node into individual bullet lists with titles as items, returning None (if no containing titles are found) or a new node. @@ -1593,7 +1585,6 @@ class BuildEnvironment: def check_consistency(self): """Do consistency checks.""" - for docname in sorted(self.all_docs): if docname not in self.files_to_rebuild: if docname == self.config.master_doc: diff --git a/sphinx/ext/autodoc.py b/sphinx/ext/autodoc.py index eef181de..3a2476a6 100644 --- a/sphinx/ext/autodoc.py +++ b/sphinx/ext/autodoc.py @@ -85,7 +85,8 @@ def members_set_option(arg): def bool_option(arg): """Used to convert flag options to auto directives. (Instead of - directives.flag(), which returns None.)""" + directives.flag(), which returns None). + """ return True @@ -133,8 +134,7 @@ class AutodocReporter(object): # Some useful event listener factories for autodoc-process-docstring. def cut_lines(pre, post=0, what=None): - """ - Return a listener that removes the first *pre* and last *post* + """Return a listener that removes the first *pre* and last *post* lines of every docstring. If *what* is a sequence of strings, only docstrings of a type in *what* will be processed. @@ -160,9 +160,8 @@ def cut_lines(pre, post=0, what=None): return process def between(marker, what=None, keepempty=False, exclude=False): - """ - Return a listener that either keeps, or if *exclude* is True excludes, lines - between lines that match the *marker* regular expression. If no line + """Return a listener that either keeps, or if *exclude* is True excludes, + lines between lines that match the *marker* regular expression. If no line matches, the resulting docstring would be empty, so no change will be made unless *keepempty* is true. @@ -262,8 +261,7 @@ class Documenter(object): self.directive.result.append(self.indent + line, source, *lineno) def resolve_name(self, modname, parents, path, base): - """ - Resolve the module and name of the object to document given by the + """Resolve the module and name of the object to document given by the arguments and the current module/class. Must return a pair of the module name and a chain of attributes; for @@ -273,8 +271,7 @@ class Documenter(object): raise NotImplementedError('must be implemented in subclasses') def parse_name(self): - """ - Determine what module to import and what attribute to document. + """Determine what module to import and what attribute to document. Returns True and sets *self.modname*, *self.objpath*, *self.fullname*, *self.args* and *self.retann* if parsing and resolving was successful. @@ -311,8 +308,7 @@ class Documenter(object): return True def import_object(self): - """ - Import the object given by *self.modname* and *self.objpath* and sets + """Import the object given by *self.modname* and *self.objpath* and set it as *self.object*. Returns True if successful, False if an error occurred. @@ -338,15 +334,15 @@ class Documenter(object): return False def get_real_modname(self): - """ - Get the real module name of an object to document. (It can differ - from the name of the module through which the object was imported.) + """Get the real module name of an object to document. + + It can differ from the name of the module through which the object was + imported. """ return self.get_attr(self.object, '__module__', None) or self.modname def check_module(self): - """ - Check if *self.object* is really defined in the module given by + """Check if *self.object* is really defined in the module given by *self.modname*. """ modname = self.get_attr(self.object, '__module__', None) @@ -355,25 +351,26 @@ class Documenter(object): return True def format_args(self): - """ - Format the argument signature of *self.object*. Should return None if - the object does not have a signature. + """Format the argument signature of *self.object*. + + Should return None if the object does not have a signature. """ return None def format_name(self): - """ - Format the name of *self.object*. This normally should be something - that can be parsed by the generated directive, but doesn't need to be - (Sphinx will display it unparsed then). + """Format the name of *self.object*. + + This normally should be something that can be parsed by the generated + directive, but doesn't need to be (Sphinx will display it unparsed + then). """ # normally the name doesn't contain the module (except for module # directives of course) return '.'.join(self.objpath) or self.modname def format_signature(self): - """ - Format the signature (arguments and return annotation) of the object. + """Format the signature (arguments and return annotation) of the object. + Let the user process it via the ``autodoc-process-signature`` event. """ if self.args is not None: @@ -473,8 +470,7 @@ class Documenter(object): self.add_line(line, src[0], src[1]) def get_object_members(self, want_all): - """ - Return `(members_check_module, members)` where `members` is a + """Return `(members_check_module, members)` where `members` is a list of `(membername, member)` pairs of the members of *self.object*. If *want_all* is True, return all members. Else, only return those @@ -518,8 +514,9 @@ class Documenter(object): return False, sorted(members) def filter_members(self, members, want_all): - """ - Filter the given member list: members are skipped if + """Filter the given member list. + + Members are skipped if - they are private (except if given explicitly) - they are undocumented (except if undoc-members is given) @@ -572,9 +569,10 @@ class Documenter(object): return ret def document_members(self, all_members=False): - """ - Generate reST for member documentation. If *all_members* is True, - do all members, else those given by *self.options.members*. + """Generate reST for member documentation. + + If *all_members* is True, do all members, else those given by + *self.options.members*. """ # set current namespace for finding members self.env.temp_data['autodoc:module'] = self.modname @@ -632,8 +630,8 @@ class Documenter(object): def generate(self, more_content=None, real_modname=None, check_module=False, all_members=False): - """ - Generate reST for the object given by *self.name*, and possibly members. + """Generate reST for the object given by *self.name*, and possibly for + its members. If *more_content* is given, include that content. If *real_modname* is given, use that module name to find attribute docs. If *check_module* is diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index cf67c7fb..8186a2e5 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -73,8 +73,7 @@ class autosummary_toc(nodes.comment): pass def process_autosummary_toc(app, doctree): - """ - Insert items described in autosummary:: to the TOC tree, but do + """Insert items described in autosummary:: to the TOC tree, but do not generate the toctree:: list. """ env = app.builder.env @@ -135,8 +134,8 @@ except AttributeError: isgetsetdescriptor = ismemberdescriptor def get_documenter(obj): - """ - Get an autodoc.Documenter class suitable for documenting the given object + """Get an autodoc.Documenter class suitable for documenting the given + object. """ import sphinx.ext.autodoc as autodoc @@ -218,8 +217,7 @@ class Autosummary(Directive): return self.warnings + nodes def get_items(self, names): - """ - Try to import the given names, and return a list of + """Try to import the given names, and return a list of ``[(name, signature, summary_string, real_name), ...]``. """ env = self.state.document.settings.env @@ -287,8 +285,7 @@ class Autosummary(Directive): return items def get_table(self, items): - """ - Generate a proper list of table nodes for autosummary:: directive. + """Generate a proper list of table nodes for autosummary:: directive. *items* is a list produced by :meth:`get_items`. """ @@ -351,8 +348,7 @@ def mangle_signature(sig, max_chars=30): return u"(%s)" % sig def limited_join(sep, items, max_chars=30, overflow_marker="..."): - """ - Join a number of strings to one, limiting the length to *max_chars*. + """Join a number of strings to one, limiting the length to *max_chars*. If the string overflows this limit, replace the last fitting item by *overflow_marker*. @@ -377,8 +373,7 @@ def limited_join(sep, items, max_chars=30, overflow_marker="..."): # -- Importing items ----------------------------------------------------------- def import_by_name(name, prefixes=[None]): - """ - Import a Python object that has the given *name*, under one of the + """Import a Python object that has the given *name*, under one of the *prefixes*. The first name that succeeds is used. """ tried = [] @@ -435,8 +430,7 @@ def _import_by_name(name): def autolink_role(typ, rawtext, etext, lineno, inliner, options={}, content=[]): - """ - Smart linking role. + """Smart linking role. Expands to ':obj:`text`' if `text` is an object that can be imported; otherwise expands to '*text*'. diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index 66a124d2..4b6348b5 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -17,6 +17,7 @@ :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ + import os import re import sys @@ -193,8 +194,8 @@ def generate_autosummary_docs(sources, output_dir=None, suffix='.rst', # -- Finding documented entries in files --------------------------------------- def find_autosummary_in_files(filenames): - """ - Find out what items are documented in source/*.rst. + """Find out what items are documented in source/*.rst. + See `find_autosummary_in_lines`. """ documented = [] @@ -206,8 +207,8 @@ def find_autosummary_in_files(filenames): return documented def find_autosummary_in_docstring(name, module=None, filename=None): - """ - Find out what items are documented in the given object's docstring. + """Find out what items are documented in the given object's docstring. + See `find_autosummary_in_lines`. """ try: @@ -221,8 +222,8 @@ def find_autosummary_in_docstring(name, module=None, filename=None): return [] def find_autosummary_in_lines(lines, module=None, filename=None): - """ - Find out what items appear in autosummary:: directives in the given lines. + """Find out what items appear in autosummary:: directives in the + given lines. Returns a list of (name, toctree, template) where *name* is a name of an object and *toctree* the :toctree: path of the corresponding diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index 257ff1b6..1fe2ac4b 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.py @@ -89,9 +89,7 @@ class GraphvizSimple(Directive): def render_dot(self, code, options, format, prefix='graphviz'): - """ - Render graphviz code into a PNG or PDF output file. - """ + """Render graphviz code into a PNG or PDF output file.""" hashkey = code.encode('utf-8') + str(options) + \ str(self.builder.config.graphviz_dot) + \ str(self.builder.config.graphviz_dot_args) diff --git a/sphinx/ext/inheritance_diagram.py b/sphinx/ext/inheritance_diagram.py index a12bad25..c697a413 100644 --- a/sphinx/ext/inheritance_diagram.py +++ b/sphinx/ext/inheritance_diagram.py @@ -67,8 +67,7 @@ class InheritanceGraph(object): graphviz dot graph from them. """ def __init__(self, class_names, currmodule, show_builtins=False): - """ - *class_names* is a list of child classes to show bases from. + """*class_names* is a list of child classes to show bases from. If *show_builtins* is True, then Python builtins will be shown in the graph. @@ -82,9 +81,7 @@ class InheritanceGraph(object): self.show_builtins = show_builtins def _import_class_or_module(self, name, currmodule): - """ - Import a class using its fully-qualified *name*. - """ + """Import a class using its fully-qualified *name*.""" try: path, base = class_sig_re.match(name).groups() except ValueError: @@ -129,18 +126,14 @@ class InheritanceGraph(object): 'not a class or module' % name) def _import_classes(self, class_names, currmodule): - """ - Import a list of classes. - """ + """Import a list of classes.""" classes = [] for name in class_names: classes.extend(self._import_class_or_module(name, currmodule)) return classes def _all_classes(self, classes): - """ - Return a list of all classes that are ancestors of *classes*. - """ + """Return a list of all classes that are ancestors of *classes*.""" all_classes = {} def recurse(cls): @@ -155,10 +148,10 @@ class InheritanceGraph(object): return all_classes.keys() def class_name(self, cls, parts=0): - """ - Given a class object, return a fully-qualified name. This - works for things I've tested in matplotlib so far, but may not - be completely general. + """Given a class object, return a fully-qualified name. + + This works for things I've tested in matplotlib so far, but may not be + completely general. """ module = cls.__module__ if module == '__builtin__': @@ -171,9 +164,7 @@ class InheritanceGraph(object): return '.'.join(name_parts[-parts:]) def get_all_class_names(self): - """ - Get all of the class names involved in the graph. - """ + """Get all of the class names involved in the graph.""" return [self.class_name(x) for x in self.all_classes] # These are the default attrs for graphviz @@ -202,9 +193,8 @@ class InheritanceGraph(object): def generate_dot(self, name, parts=0, urls={}, env=None, graph_attrs={}, node_attrs={}, edge_attrs={}): - """ - Generate a graphviz dot graph from the classes that - were passed in to __init__. + """Generate a graphviz dot graph from the classes that were passed in + to __init__. *name* is the name of the graph. diff --git a/sphinx/ext/oldcmarkup.py b/sphinx/ext/oldcmarkup.py index 00ac3749..bc921a23 100644 --- a/sphinx/ext/oldcmarkup.py +++ b/sphinx/ext/oldcmarkup.py @@ -18,6 +18,7 @@ WARNING_MSG = 'using old C markup; please migrate to new-style markup ' \ '(e.g. c:function instead of cfunction), see ' \ 'http://sphinx.pocoo.org/domains.html' + class OldCDirective(Directive): has_content = True required_arguments = 1 diff --git a/sphinx/ext/pngmath.py b/sphinx/ext/pngmath.py index 7f399754..5903c53d 100644 --- a/sphinx/ext/pngmath.py +++ b/sphinx/ext/pngmath.py @@ -61,8 +61,7 @@ DOC_BODY_PREVIEW = r''' depth_re = re.compile(r'\[\d+ depth=(-?\d+)\]') def render_math(self, math): - """ - Render the LaTeX math expression *math* using latex and dvipng. + """Render the LaTeX math expression *math* using latex and dvipng. Return the filename relative to the built document and the "depth", that is, the distance of image bottom and baseline in pixels, if the diff --git a/sphinx/jinja2glue.py b/sphinx/jinja2glue.py index a6f1a853..29ee334e 100644 --- a/sphinx/jinja2glue.py +++ b/sphinx/jinja2glue.py @@ -37,8 +37,10 @@ def accesskey(context, key): class SphinxFileSystemLoader(FileSystemLoader): - """FileSystemLoader subclass that is not so strict about '..' - entries in template names.""" + """ + FileSystemLoader subclass that is not so strict about '..' entries in + template names. + """ def get_source(self, environment, template): for searchpath in self.searchpath: diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py index de106bb9..126a37b5 100644 --- a/sphinx/locale/__init__.py +++ b/sphinx/locale/__init__.py @@ -15,8 +15,9 @@ import UserString class _TranslationProxy(UserString.UserString, object): - """Class for proxy strings from gettext translations. This is a helper - for the lazy_* functions from this module. + """ + Class for proxy strings from gettext translations. This is a helper for the + lazy_* functions from this module. The proxy implementation attempts to be as complete as possible, so that the lazy objects should mostly work as expected, for example for sorting. @@ -137,7 +138,8 @@ class _TranslationProxy(UserString.UserString, object): def mygettext(string): """Used instead of _ when creating TranslationProxies, because _ is - not bound yet at that time.""" + not bound yet at that time. + """ return _(string) def lazy_gettext(string): @@ -189,8 +191,7 @@ else: def init(locale_dirs, language, catalog='sphinx'): - """ - Look for message catalogs in `locale_dirs` and *ensure* that there is at + """Look for message catalogs in `locale_dirs` and *ensure* that there is at least a NullTranslations catalog set in `translators`. If called multiple times or if several ``.mo`` files are found, their contents are merged together (thus making ``init`` reentrable). diff --git a/sphinx/roles.py b/sphinx/roles.py index 0ea0ec48..f08c3f00 100644 --- a/sphinx/roles.py +++ b/sphinx/roles.py @@ -139,16 +139,15 @@ class XRefRole(object): # methods that can be overwritten def process_link(self, env, refnode, has_explicit_title, title, target): - """ - Called after parsing title and target text, and creating the reference - node (given in *refnode*). This method can alter the reference node and - must return a new (or the same) ``(title, target)`` tuple. + """Called after parsing title and target text, and creating the + reference node (given in *refnode*). This method can alter the + reference node and must return a new (or the same) ``(title, target)`` + tuple. """ return title, ws_re.sub(' ', target) def result_nodes(self, document, env, node, is_ref): - """ - Called before returning the finished nodes. *node* is the reference + """Called before returning the finished nodes. *node* is the reference node if one was created (*is_ref* is then true), else the content node. This method can add other nodes and must return a ``(nodes, messages)`` tuple (the usual return value of a role function). diff --git a/sphinx/setup_command.py b/sphinx/setup_command.py index 8974b988..865ebb67 100644 --- a/sphinx/setup_command.py +++ b/sphinx/setup_command.py @@ -22,7 +22,8 @@ from sphinx.util.console import darkred, nocolor, color_terminal class BuildDoc(Command): - """Distutils command to build Sphinx documentation. + """ + Distutils command to build Sphinx documentation. The Sphinx build can then be triggered from distutils, and some Sphinx options can be set in ``setup.py`` or ``setup.cfg`` instead of Sphinx own diff --git a/sphinx/theming.py b/sphinx/theming.py index 0d0f2863..92e63f31 100644 --- a/sphinx/theming.py +++ b/sphinx/theming.py @@ -98,8 +98,7 @@ class Theme(object): self.base = Theme(inherit) def get_confstr(self, section, name, default=NODEFAULT): - """ - Return the value for a theme configuration setting, searching the + """Return the value for a theme configuration setting, searching the base theme chain. """ try: @@ -114,9 +113,7 @@ class Theme(object): return default def get_options(self, overrides): - """ - Return a dictionary of theme options and their values. - """ + """Return a dictionary of theme options and their values.""" chain = [self.themeconf] base = self.base while base is not None: @@ -135,8 +132,7 @@ class Theme(object): return options def get_dirchain(self): - """ - Return a list of theme directories, beginning with this theme's, + """Return a list of theme directories, beginning with this theme's, then the base theme's, then that one's base theme's, etc. """ chain = [self.themedir] @@ -147,9 +143,7 @@ class Theme(object): return chain def cleanup(self): - """ - Remove temporary directories. - """ + """Remove temporary directories.""" if self.themedir_created: try: shutil.rmtree(self.themedir) diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index 6a38351f..a3d30d9d 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -50,8 +50,7 @@ def docname_join(basedocname, docname): def get_matching_files(dirname, exclude_matchers=()): - """ - Get all file names in a directory, recursively. + """Get all file names in a directory, recursively. Exclude files and dirs matching some matcher in *exclude_matchers*. """ @@ -77,9 +76,8 @@ def get_matching_files(dirname, exclude_matchers=()): def get_matching_docs(dirname, suffix, exclude_matchers=()): - """ - Get all file names (without suffix) matching a suffix in a - directory, recursively. + """Get all file names (without suffix) matching a suffix in a directory, + recursively. Exclude files and dirs matching a pattern in *exclude_patterns*. """ @@ -171,9 +169,7 @@ _DEBUG_HEADER = '''\ ''' def save_traceback(): - """ - Save the current exception's traceback in a temporary file. - """ + """Save the current exception's traceback in a temporary file.""" exc = traceback.format_exc() fd, path = tempfile.mkstemp('.log', 'sphinx-err-') os.write(fd, (_DEBUG_HEADER % @@ -233,8 +229,7 @@ class Tee(object): def parselinenos(spec, total): - """ - Parse a line number spec (such as "1,2,4-6") and return a list of + """Parse a line number spec (such as "1,2,4-6") and return a list of wanted line numbers. """ items = list() @@ -288,9 +283,7 @@ def rpartition(s, t): def format_exception_cut_frames(x=1): - """ - Format an exception with traceback, but only the last x frames. - """ + """Format an exception with traceback, but only the last x frames.""" typ, val, tb = sys.exc_info() #res = ['Traceback (most recent call last):\n'] res = [] diff --git a/sphinx/util/docstrings.py b/sphinx/util/docstrings.py index 538af653..d1a2ff8d 100644 --- a/sphinx/util/docstrings.py +++ b/sphinx/util/docstrings.py @@ -13,11 +13,11 @@ import sys def prepare_docstring(s): - """ - Convert a docstring into lines of parseable reST. Return it as a list of - lines usable for inserting into a docutils ViewList (used as argument - of nested_parse().) An empty line is added to act as a separator between - this docstring and following content. + """Convert a docstring into lines of parseable reST. + + Return it as a list of lines usable for inserting into a docutils ViewList + (used as argument of nested_parse().) An empty line is added to act as a + separator between this docstring and following content. """ lines = s.expandtabs().splitlines() # Find minimum indentation of any non-blank lines after first line. @@ -42,9 +42,8 @@ def prepare_docstring(s): def prepare_commentdoc(s): - """ - Extract documentation comment lines (starting with #:) and return them as a - list of lines. Returns an empty list if there is no documentation. + """Extract documentation comment lines (starting with #:) and return them + as a list of lines. Returns an empty list if there is no documentation. """ result = [] lines = [line.strip() for line in s.expandtabs().splitlines()] diff --git a/sphinx/util/jsonimpl.py b/sphinx/util/jsonimpl.py index fda85b5e..f654ef22 100644 --- a/sphinx/util/jsonimpl.py +++ b/sphinx/util/jsonimpl.py @@ -13,7 +13,7 @@ import UserString try: import json - # json-py's json module has not JSONEncoder; this will raise AttributeError + # json-py's json module has no JSONEncoder; this will raise AttributeError # if json-py is imported instead of the built-in json module JSONEncoder = json.JSONEncoder except (ImportError, AttributeError): diff --git a/sphinx/util/matching.py b/sphinx/util/matching.py index c459aca2..3746c87c 100644 --- a/sphinx/util/matching.py +++ b/sphinx/util/matching.py @@ -13,8 +13,7 @@ import re def _translate_pattern(pat): - """ - Translate a shell-style glob pattern to a regular expression. + """Translate a shell-style glob pattern to a regular expression. Adapted from the fnmatch module, but enhanced so that single stars don't match slashes. @@ -65,16 +64,14 @@ def compile_matchers(patterns): _pat_cache = {} def patmatch(name, pat): - """ - Return if name matches pat. Adapted from fnmatch module. - """ + """Return if name matches pat. Adapted from fnmatch module.""" if pat not in _pat_cache: _pat_cache[pat] = re.compile(_translate_pattern(pat)) return _pat_cache[pat].match(name) def patfilter(names, pat): - """ - Return the subset of the list NAMES that match PAT. + """Return the subset of the list NAMES that match PAT. + Adapted from fnmatch module. """ if pat not in _pat_cache: diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 9ecf0d4a..2e383b0a 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -38,6 +38,12 @@ def extract_messages(doctree): def nested_parse_with_titles(state, content, node): + """Version of state.nested_parse() that allows titles and does not require + titles to have the same decoration as the calling document. + + This is useful when the parsed content comes from a completely different + context, such as docstrings. + """ # hack around title style bookkeeping surrounding_title_styles = state.memo.title_styles surrounding_section_level = state.memo.section_level diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py index 9943b207..464e56ed 100644 --- a/sphinx/util/osutil.py +++ b/sphinx/util/osutil.py @@ -59,8 +59,8 @@ def ensuredir(path): def walk(top, topdown=True, followlinks=False): - """ - Backport of os.walk from 2.6, where the followlinks argument was added. + """Backport of os.walk from 2.6, where the *followlinks* argument was + added. """ names = os.listdir(top) diff --git a/sphinx/util/png.py b/sphinx/util/png.py index 2cb2aa9d..ca2273c4 100644 --- a/sphinx/util/png.py +++ b/sphinx/util/png.py @@ -21,9 +21,7 @@ IEND_CHUNK = '\x00\x00\x00\x00IEND\xAE\x42\x60\x82' def read_png_depth(filename): - """ - Read the special tEXt chunk indicating the depth from a PNG file. - """ + """Read the special tEXt chunk indicating the depth from a PNG file.""" result = None f = open(filename, 'rb') try: @@ -39,8 +37,8 @@ def read_png_depth(filename): def write_png_depth(filename, depth): - """ - Write the special tEXt chunk indicating the depth to a PNG file. + """Write the special tEXt chunk indicating the depth to a PNG file. + The chunk is placed immediately before the special IEND chunk. """ data = struct.pack('!i', depth) diff --git a/sphinx/util/websupport.py b/sphinx/util/websupport.py index f99f4d31..510ecbe0 100644 --- a/sphinx/util/websupport.py +++ b/sphinx/util/websupport.py @@ -6,5 +6,6 @@ :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ + def is_commentable(node): return node.__class__.__name__ in ('paragraph', 'literal_block') diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index 30e8c4dc..bb80fb7e 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -3,7 +3,7 @@ sphinx.writers.websupport ~~~~~~~~~~~~~~~~~~~~~~~~~ - docutils writers handling Sphinx' custom nodes. + sphinx.websupport writer that adds comment-related annotations. :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. @@ -12,6 +12,7 @@ from sphinx.writers.html import HTMLTranslator from sphinx.util.websupport import is_commentable + class WebSupportTranslator(HTMLTranslator): """ Our custom HTML translator. -- cgit v1.2.1 From 7a810a19eed8dc7664dd9878a815e368f5e51551 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Mon, 23 Aug 2010 13:06:22 +0000 Subject: List GSOC project authors. --- AUTHORS | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 10120c7b..2ee77739 100644 --- a/AUTHORS +++ b/AUTHORS @@ -15,12 +15,14 @@ Other contributors, listed alphabetically, are: * Martin Hans -- autodoc improvements * Dave Kuhlman -- original LaTeX writer * Thomas Lamb -- linkcheck builder +* Robert Lehmann -- gettext builder (GSOC project) * Dan MacKinlay -- metadata fixes * Martin Mahner -- nature theme * Will Maier -- directory HTML builder +* Jacob Mason -- websupport library (GSOC project) * Roland Meister -- epub builder * Ezio Melotti -- collapsible sidebar JavaScript -* Daniel Neuhäuser -- JavaScript domain +* Daniel Neuhäuser -- JavaScript domain, Python 3 support (GSOC) * Christopher Perkins -- autosummary integration * Benjamin Peterson -- unittests * T. Powers -- HTML output improvements -- cgit v1.2.1 From 988e61e6bbcf22ab64a535c1ae979c5197dc39c5 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Mon, 23 Aug 2010 13:07:19 +0000 Subject: Add new env method to get the real path to a file reference, and use it. --- sphinx/directives/code.py | 24 +++++---------------- sphinx/directives/other.py | 9 ++++---- sphinx/environment.py | 52 +++++++++++++++++++++++++++------------------- 3 files changed, 40 insertions(+), 45 deletions(-) diff --git a/sphinx/directives/code.py b/sphinx/directives/code.py index 1808cdab..d235c4ca 100644 --- a/sphinx/directives/code.py +++ b/sphinx/directives/code.py @@ -7,10 +7,8 @@ :license: BSD, see LICENSE for details. """ -import os import sys import codecs -from os import path from docutils import nodes from docutils.parsers.rst import Directive, directives @@ -93,23 +91,11 @@ class LiteralInclude(Directive): def run(self): document = self.state.document - filename = self.arguments[0] if not document.settings.file_insertion_enabled: return [document.reporter.warning('File insertion disabled', line=self.lineno)] env = document.settings.env - if filename.startswith('/') or filename.startswith(os.sep): - rel_fn = filename[1:] - else: - docdir = path.dirname(env.doc2path(env.docname, base=None)) - rel_fn = path.join(docdir, filename) - try: - fn = path.join(env.srcdir, rel_fn) - except UnicodeDecodeError: - # the source directory is a bytestring with non-ASCII characters; - # let's try to encode the rel_fn in the file system encoding - rel_fn = rel_fn.encode(sys.getfilesystemencoding()) - fn = path.join(env.srcdir, rel_fn) + rel_filename, filename = env.relfn2path(self.arguments[0]) if 'pyobject' in self.options and 'lines' in self.options: return [document.reporter.warning( @@ -119,7 +105,7 @@ class LiteralInclude(Directive): encoding = self.options.get('encoding', env.config.source_encoding) codec_info = codecs.lookup(encoding) try: - f = codecs.StreamReaderWriter(open(fn, 'rb'), + f = codecs.StreamReaderWriter(open(filename, 'rb'), codec_info[2], codec_info[3], 'strict') lines = f.readlines() f.close() @@ -136,7 +122,7 @@ class LiteralInclude(Directive): objectname = self.options.get('pyobject') if objectname is not None: from sphinx.pycode import ModuleAnalyzer - analyzer = ModuleAnalyzer.for_file(fn, '') + analyzer = ModuleAnalyzer.for_file(filename, '') tags = analyzer.find_tags() if objectname not in tags: return [document.reporter.warning( @@ -178,13 +164,13 @@ class LiteralInclude(Directive): text = ''.join(lines) if self.options.get('tab-width'): text = text.expandtabs(self.options['tab-width']) - retnode = nodes.literal_block(text, text, source=fn) + retnode = nodes.literal_block(text, text, source=filename) retnode.line = 1 if self.options.get('language', ''): retnode['language'] = self.options['language'] if 'linenos' in self.options: retnode['linenos'] = True - document.settings.env.note_dependency(rel_fn) + env.note_dependency(rel_filename) return [retnode] diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index 332c4084..45c64030 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -369,14 +369,13 @@ from docutils.parsers.rst.directives.misc import Include as BaseInclude class Include(BaseInclude): """ Like the standard "Include" directive, but interprets absolute paths - correctly. + "correctly", i.e. relative to source directory. """ def run(self): - if self.arguments[0].startswith('/') or \ - self.arguments[0].startswith(os.sep): - env = self.state.document.settings.env - self.arguments[0] = os.path.join(env.srcdir, self.arguments[0][1:]) + env = self.state.document.settings.env + rel_filename, filename = env.relfn2path(self.arguments[0]) + self.arguments[0] = filename return BaseInclude.run(self) diff --git a/sphinx/environment.py b/sphinx/environment.py index f8d3641b..2a48bdf2 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -433,6 +433,27 @@ class BuildEnvironment: else: return path.join(base, docname) + suffix + def relfn2path(self, filename, docname=None): + """Return paths to a file referenced from a document, relative to + documentation root and absolute. + + Absolute filenames are relative to the source dir, while relative + filenames are relative to the dir of the containing document. + """ + if filename.startswith('/') or filename.startswith(os.sep): + rel_fn = filename[1:] + else: + docdir = path.dirname(self.doc2path(docname or self.docname, + base=None)) + rel_fn = path.join(docdir, filename) + try: + return rel_fn, path.join(self.srcdir, rel_fn) + except UnicodeDecodeError: + # the source directory is a bytestring with non-ASCII characters; + # let's try to encode the rel_fn in the file system encoding + enc_rel_fn = rel_fn.encode(sys.getfilesystemencoding()) + return rel_fn, path.join(self.srcdir, enc_rel_fn) + def find_files(self, config): """Find all source files in the source dir and put them in self.found_docs. @@ -806,25 +827,19 @@ class BuildEnvironment: def process_downloads(self, docname, doctree): """Process downloadable file paths. """ - docdir = path.dirname(self.doc2path(docname, base=None)) for node in doctree.traverse(addnodes.download_reference): targetname = node['reftarget'] - if targetname.startswith('/') or targetname.startswith(os.sep): - # absolute - filepath = targetname[1:] - else: - filepath = path.normpath(path.join(docdir, node['reftarget'])) - self.dependencies.setdefault(docname, set()).add(filepath) - if not os.access(path.join(self.srcdir, filepath), os.R_OK): - self.warn(docname, 'download file not readable: %s' % filepath, + rel_filename, filename = self.relfn2path(targetname, docname) + self.dependencies.setdefault(docname, set()).add(rel_filename) + if not os.access(filename, os.R_OK): + self.warn(docname, 'download file not readable: %s' % filename, getattr(node, 'line', None)) continue - uniquename = self.dlfiles.add_file(docname, filepath) + uniquename = self.dlfiles.add_file(docname, filename) node['filename'] = uniquename def process_images(self, docname, doctree): """Process and rewrite image URIs.""" - docdir = path.dirname(self.doc2path(docname, base=None)) for node in doctree.traverse(nodes.image): # Map the mimetype to the corresponding image. The writer may # choose the best image from these candidates. The special key * is @@ -837,16 +852,11 @@ class BuildEnvironment: node.line) candidates['?'] = imguri continue - # imgpath is the image path *from srcdir* - if imguri.startswith('/') or imguri.startswith(os.sep): - # absolute path (= relative to srcdir) - imgpath = path.normpath(imguri[1:]) - else: - imgpath = path.normpath(path.join(docdir, imguri)) + rel_imgpath, full_imgpath = self.relfn2path(imguri, docname) # set imgpath as default URI - node['uri'] = imgpath - if imgpath.endswith(os.extsep + '*'): - for filename in glob(path.join(self.srcdir, imgpath)): + node['uri'] = rel_imgpath + if rel_imgpath.endswith(os.extsep + '*'): + for filename in glob(full_imgpath): new_imgpath = relative_path(self.srcdir, filename) if filename.lower().endswith('.pdf'): candidates['application/pdf'] = new_imgpath @@ -866,7 +876,7 @@ class BuildEnvironment: if imgtype: candidates['image/' + imgtype] = new_imgpath else: - candidates['*'] = imgpath + candidates['*'] = rel_imgpath # map image paths to unique image names (so that they can be put # into a single directory) for imgpath in candidates.itervalues(): -- cgit v1.2.1 From b817147065b0c4116f19303412268d920e3e1230 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Mon, 23 Aug 2010 13:07:28 +0000 Subject: #443: Allow referencing external graphviz files. --- CHANGES | 7 +++++++ doc/ext/graphviz.rst | 11 +++++++++++ sphinx/ext/graphviz.py | 33 +++++++++++++++++++++++++++------ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/CHANGES b/CHANGES index 3417d9aa..d46d2288 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,13 @@ Release 1.1 (in development) * Added Python 3.x support. +* Added i18n support for content, a ``gettext`` builder and + related utilities. + +* Added the ``websupport`` library. + +* #443: Allow referencing external graphviz files. + Release 1.0.2 (Aug 14, 2010) ============================ diff --git a/doc/ext/graphviz.rst b/doc/ext/graphviz.rst index 3741cec6..de6e03e2 100644 --- a/doc/ext/graphviz.rst +++ b/doc/ext/graphviz.rst @@ -29,6 +29,17 @@ It adds these directives: :confval:`graphviz_output_format`). In LaTeX output, the code will be rendered to an embeddable PDF file. + You can also embed external dot files, by giving the file name as an + argument to :rst:dir:`graphviz` and no additional content:: + + .. graphviz:: external.dot + + As for all file references in Sphinx, if the filename is absolute, it is + taken as relative to the source directory. + + .. versionchanged:: 1.1 + Added support for external files. + .. rst:directive:: graph diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index 1fe2ac4b..19dcd951 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.py @@ -11,6 +11,7 @@ """ import re +import codecs import posixpath from os import path from math import ceil @@ -46,18 +47,38 @@ class Graphviz(Directive): """ has_content = True required_arguments = 0 - optional_arguments = 0 + optional_arguments = 1 final_argument_whitespace = False option_spec = { 'alt': directives.unchanged, } def run(self): - dotcode = '\n'.join(self.content) - if not dotcode.strip(): - return [self.state_machine.reporter.warning( - 'Ignoring "graphviz" directive without content.', - line=self.lineno)] + if self.arguments: + document = self.state.document + if self.content: + return [document.reporter.warning( + 'Graphviz directive cannot have both content and ' + 'a filename argument', line=self.lineno)] + env = self.state.document.settings.env + rel_filename, filename = env.relfn2path(self.arguments[0]) + env.note_dependency(rel_filename) + try: + fp = codecs.open(filename, 'r', 'utf-8') + try: + dotcode = fp.read() + finally: + fp.close() + except (IOError, OSError): + return [document.reporter.warning( + 'External Graphviz file %r not found or reading ' + 'it failed' % filename, line=self.lineno)] + else: + dotcode = '\n'.join(self.content) + if not dotcode.strip(): + return [self.state_machine.reporter.warning( + 'Ignoring "graphviz" directive without content.', + line=self.lineno)] node = graphviz() node['code'] = dotcode node['options'] = [] -- cgit v1.2.1 From c3028914ee7a2fc8e82133be4b891aec02d95a92 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Mon, 23 Aug 2010 18:16:19 +0200 Subject: Use b() so that the regex is a bytestring --- sphinx/ext/pngmath.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sphinx/ext/pngmath.py b/sphinx/ext/pngmath.py index 5903c53d..e4e7c2d0 100644 --- a/sphinx/ext/pngmath.py +++ b/sphinx/ext/pngmath.py @@ -26,6 +26,7 @@ from docutils import nodes from sphinx.errors import SphinxError from sphinx.util.png import read_png_depth, write_png_depth from sphinx.util.osutil import ensuredir, ENOENT +from sphinx.util.pycompat import b from sphinx.ext.mathbase import setup_math as mathbase_setup, wrap_displaymath class MathExtError(SphinxError): @@ -58,7 +59,7 @@ DOC_BODY_PREVIEW = r''' \end{document} ''' -depth_re = re.compile(r'\[\d+ depth=(-?\d+)\]') +depth_re = re.compile(b(r'\[\d+ depth=(-?\d+)\]')) def render_math(self, math): """Render the LaTeX math expression *math* using latex and dvipng. -- cgit v1.2.1 From 2274fedf69996bfe3643234020ac6ca07495f992 Mon Sep 17 00:00:00 2001 From: DasIch <dasdasich@gmail.com> Date: Mon, 23 Aug 2010 18:18:04 +0200 Subject: Make chunks bytestrings --- sphinx/util/png.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sphinx/util/png.py b/sphinx/util/png.py index ca2273c4..59c32715 100644 --- a/sphinx/util/png.py +++ b/sphinx/util/png.py @@ -12,12 +12,14 @@ import struct import binascii +from sphinx.util.pycompat import b + LEN_IEND = 12 LEN_DEPTH = 22 DEPTH_CHUNK_LEN = struct.pack('!i', 10) -DEPTH_CHUNK_START = 'tEXtDepth\x00' -IEND_CHUNK = '\x00\x00\x00\x00IEND\xAE\x42\x60\x82' +DEPTH_CHUNK_START = b('tEXtDepth\x00') +IEND_CHUNK = b('\x00\x00\x00\x00IEND\xAE\x42\x60\x82') def read_png_depth(filename): -- cgit v1.2.1 From 1a66c02c8f588a4a6324c3e5b9cf42b736bc1f40 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Wed, 25 Aug 2010 09:59:53 +0000 Subject: #460: Allow limiting the depth of section numbers for HTML. --- CHANGES | 2 ++ doc/markup/toctree.rst | 14 +++++++++++++- sphinx/directives/other.py | 10 ++++++++-- sphinx/environment.py | 28 ++++++++++++++++++---------- sphinx/writers/html.py | 6 +++--- 5 files changed, 44 insertions(+), 16 deletions(-) diff --git a/CHANGES b/CHANGES index 79012cfe..6f89ae15 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Release 1.1 (in development) * Added the ``websupport`` library. +* #460: Allow limiting the depth of section numbers for HTML. + * #443: Allow referencing external graphviz files. diff --git a/doc/markup/toctree.rst b/doc/markup/toctree.rst index 2c0a418a..0b6a46c1 100644 --- a/doc/markup/toctree.rst +++ b/doc/markup/toctree.rst @@ -41,6 +41,8 @@ tables of contents. The ``toctree`` directive is the central element. document, the library index. From this information it generates "next chapter", "previous chapter" and "parent chapter" links. + **Entries** + Document titles in the :rst:dir:`toctree` will be automatically read from the title of the referenced document. If that isn't what you want, you can specify an explicit title and target using a similar syntax to reST @@ -59,8 +61,10 @@ tables of contents. The ``toctree`` directive is the central element. You can also add external links, by giving an HTTP URL instead of a document name. + **Section numbering** + If you want to have section numbers even in HTML output, give the toctree a - ``numbered`` flag option. For example:: + ``numbered`` option. For example:: .. toctree:: :numbered: @@ -71,6 +75,11 @@ tables of contents. The ``toctree`` directive is the central element. Numbering then starts at the heading of ``foo``. Sub-toctrees are automatically numbered (don't give the ``numbered`` flag to those). + Numbering up to a specific depth is also possible, by giving the depth as a + numeric argument to ``numbered``. + + **Additional options** + If you want only the titles of documents in the tree to show up, not other headings of the same level, you can use the ``titlesonly`` option:: @@ -133,6 +142,9 @@ tables of contents. The ``toctree`` directive is the central element. .. versionchanged:: 1.0 Added "titlesonly" option. + .. versionchanged:: 1.1 + Added numeric argument to "numbered". + Special names ------------- diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index 45c64030..b5252e86 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -20,6 +20,12 @@ from sphinx.util.compat import make_admonition from sphinx.util.matching import patfilter +def int_or_nothing(argument): + if not argument: + return 999 + return int(argument) + + class TocTree(Directive): """ Directive to notify Sphinx about the hierarchical structure of the docs, @@ -34,7 +40,7 @@ class TocTree(Directive): 'maxdepth': int, 'glob': directives.flag, 'hidden': directives.flag, - 'numbered': directives.flag, + 'numbered': int_or_nothing, 'titlesonly': directives.flag, } @@ -98,7 +104,7 @@ class TocTree(Directive): subnode['maxdepth'] = self.options.get('maxdepth', -1) subnode['glob'] = glob subnode['hidden'] = 'hidden' in self.options - subnode['numbered'] = 'numbered' in self.options + subnode['numbered'] = self.options.get('numbered', 0) subnode['titlesonly'] = 'titlesonly' in self.options wrappernode = nodes.compound(classes=['toctree-wrapper']) wrappernode.append(subnode) diff --git a/sphinx/environment.py b/sphinx/environment.py index 4a19968c..883c91e9 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -1426,46 +1426,54 @@ class BuildEnvironment: old_secnumbers = self.toc_secnumbers self.toc_secnumbers = {} - def _walk_toc(node, secnums, titlenode=None): + def _walk_toc(node, secnums, depth, titlenode=None): # titlenode is the title of the document, it will get assigned a # secnumber too, so that it shows up in next/prev/parent rellinks for subnode in node.children: if isinstance(subnode, nodes.bullet_list): numstack.append(0) - _walk_toc(subnode, secnums, titlenode) + _walk_toc(subnode, secnums, depth-1, titlenode) numstack.pop() titlenode = None elif isinstance(subnode, nodes.list_item): - _walk_toc(subnode, secnums, titlenode) + _walk_toc(subnode, secnums, depth, titlenode) titlenode = None elif isinstance(subnode, addnodes.compact_paragraph): numstack[-1] += 1 + if depth > 0: + number = tuple(numstack) + else: + number = None secnums[subnode[0]['anchorname']] = \ - subnode[0]['secnumber'] = tuple(numstack) + subnode[0]['secnumber'] = number if titlenode: - titlenode['secnumber'] = tuple(numstack) + titlenode['secnumber'] = number titlenode = None elif isinstance(subnode, addnodes.toctree): - _walk_toctree(subnode) + _walk_toctree(subnode, depth) - def _walk_toctree(toctreenode): + def _walk_toctree(toctreenode, depth): + if depth == 0: + return for (title, ref) in toctreenode['entries']: if url_re.match(ref) or ref == 'self': # don't mess with those continue if ref in self.tocs: secnums = self.toc_secnumbers[ref] = {} - _walk_toc(self.tocs[ref], secnums, self.titles.get(ref)) + _walk_toc(self.tocs[ref], secnums, depth, + self.titles.get(ref)) if secnums != old_secnumbers.get(ref): rewrite_needed.append(ref) for docname in self.numbered_toctrees: doctree = self.get_doctree(docname) for toctreenode in doctree.traverse(addnodes.toctree): - if toctreenode.get('numbered'): + depth = toctreenode.get('numbered', 0) + if depth: # every numbered toctree gets new numbering numstack = [0] - _walk_toctree(toctreenode) + _walk_toctree(toctreenode, depth) return rewrite_needed diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py index f206e479..33d90c91 100644 --- a/sphinx/writers/html.py +++ b/sphinx/writers/html.py @@ -180,7 +180,7 @@ class HTMLTranslator(BaseTranslator): atts['title'] = node['reftitle'] self.body.append(self.starttag(node, 'a', '', **atts)) - if node.hasattr('secnumber'): + if node.get('secnumber'): self.body.append(('%s' + self.secnumber_suffix) % '.'.join(map(str, node['secnumber']))) @@ -202,14 +202,14 @@ class HTMLTranslator(BaseTranslator): self.depart_admonition(node) def add_secnumber(self, node): - if node.hasattr('secnumber'): + if node.get('secnumber'): self.body.append('.'.join(map(str, node['secnumber'])) + self.secnumber_suffix) elif isinstance(node.parent, nodes.section): anchorname = '#' + node.parent['ids'][0] if anchorname not in self.builder.secnumbers: anchorname = '' # try first heading which has no anchor - if anchorname in self.builder.secnumbers: + if self.builder.secnumbers.get(anchorname): numbers = self.builder.secnumbers[anchorname] self.body.append('.'.join(map(str, numbers)) + self.secnumber_suffix) -- cgit v1.2.1 From ca1e0e0048f98efe01175a2644f4dce6a27127fa Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Wed, 25 Aug 2010 10:09:24 +0000 Subject: #221: Add Swedish locale, thanks to Henrik Holmboe. --- CHANGES | 2 + doc/config.rst | 1 + sphinx/locale/sv/LC_MESSAGES/sphinx.js | 1 + sphinx/locale/sv/LC_MESSAGES/sphinx.mo | Bin 0 -> 9511 bytes sphinx/locale/sv/LC_MESSAGES/sphinx.po | 794 +++++++++++++++++++++++++++++++++ 5 files changed, 798 insertions(+) create mode 100644 sphinx/locale/sv/LC_MESSAGES/sphinx.js create mode 100644 sphinx/locale/sv/LC_MESSAGES/sphinx.mo create mode 100644 sphinx/locale/sv/LC_MESSAGES/sphinx.po diff --git a/CHANGES b/CHANGES index 6f89ae15..45467a16 100644 --- a/CHANGES +++ b/CHANGES @@ -12,6 +12,8 @@ Release 1.1 (in development) * #443: Allow referencing external graphviz files. +* #221: Add Swedish locale. + Release 1.0.4 (in development) ============================== diff --git a/doc/config.rst b/doc/config.rst index e0fbeb46..3115c401 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -300,6 +300,7 @@ Project information * ``pt_BR`` -- Brazilian Portuguese * ``ru`` -- Russian * ``sl`` -- Slovenian + * ``sv`` -- Swedish * ``tr`` -- Turkish * ``uk_UA`` -- Ukrainian * ``zh_CN`` -- Simplified Chinese diff --git a/sphinx/locale/sv/LC_MESSAGES/sphinx.js b/sphinx/locale/sv/LC_MESSAGES/sphinx.js new file mode 100644 index 00000000..0cedfb45 --- /dev/null +++ b/sphinx/locale/sv/LC_MESSAGES/sphinx.js @@ -0,0 +1 @@ +Documentation.addTranslations({"locale": "sv", "plural_expr": "(n != 1)", "messages": {"Search Results": "S\u00f6kresultat", "Preparing search...": "F\u00f6rbereder s\u00f6kning...", "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories.": "Din s\u00f6kning gav inga resultat. Kolla stavning och att du valt tillr\u00e4ckligt med kategorier.", "Search finished, found %s page(s) matching the search query.": "S\u00f6kning f\u00e4rdig, hittade %s tr\u00e4ffar.", ", in ": ", i ", "Expand sidebar": "Expandera sidolist", "Permalink to this headline": "Permalink till denna rubrik", "Searching": "S\u00f6ker", "Collapse sidebar": "D\u00f6lj sidolist", "Permalink to this definition": "Permalink till denna definition", "Hide Search Matches": "D\u00f6lj S\u00f6kresultat"}}); \ No newline at end of file diff --git a/sphinx/locale/sv/LC_MESSAGES/sphinx.mo b/sphinx/locale/sv/LC_MESSAGES/sphinx.mo new file mode 100644 index 00000000..203ead14 Binary files /dev/null and b/sphinx/locale/sv/LC_MESSAGES/sphinx.mo differ diff --git a/sphinx/locale/sv/LC_MESSAGES/sphinx.po b/sphinx/locale/sv/LC_MESSAGES/sphinx.po new file mode 100644 index 00000000..f5f3b7e4 --- /dev/null +++ b/sphinx/locale/sv/LC_MESSAGES/sphinx.po @@ -0,0 +1,794 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2010-05-24 23:53+0200\n" +"PO-Revision-Date: \n" +"Last-Translator: Henrik Holmboe <henrik@holmboe.se>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Poedit-Language: Swedish\n" +"X-Poedit-Country: SWEDEN\n" +"X-Poedit-Basepath: /home/hh/Local/src/sphinx\n" + +#: sphinx/environment.py:106 +#: sphinx/writers/latex.py:184 +#: sphinx/writers/manpage.py:67 +#, python-format +msgid "%B %d, %Y" +msgstr "%B %d, %Y" + +#: sphinx/roles.py:174 +#, python-format +msgid "Python Enhancement Proposals!PEP %s" +msgstr "Python Enhancement Proposals!PEP %s" + +#: sphinx/builders/changes.py:72 +msgid "Builtins" +msgstr "Inbyggda" + +#: sphinx/builders/changes.py:74 +msgid "Module level" +msgstr "Modulnivå" + +#: sphinx/builders/html.py:266 +#, python-format +msgid "%b %d, %Y" +msgstr "%b %d, %Y" + +#: sphinx/builders/html.py:285 +#: sphinx/themes/basic/defindex.html:30 +msgid "General Index" +msgstr "Huvudindex" + +#: sphinx/builders/html.py:285 +msgid "index" +msgstr "index" + +#: sphinx/builders/html.py:345 +msgid "next" +msgstr "nästa" + +#: sphinx/builders/html.py:354 +msgid "previous" +msgstr "föregående" + +#: sphinx/builders/latex.py:151 +msgid " (in " +msgstr "(i " + +#: sphinx/directives/other.py:127 +msgid "Section author: " +msgstr "Sektionsförfattare" + +#: sphinx/directives/other.py:129 +msgid "Module author: " +msgstr "Modulförfattare" + +#: sphinx/directives/other.py:131 +msgid "Code author: " +msgstr "Källkodsförfattare" + +#: sphinx/directives/other.py:133 +msgid "Author: " +msgstr "Upphovsman:" + +#: sphinx/directives/other.py:238 +msgid "See also" +msgstr "Visa även" + +#: sphinx/domains/__init__.py:253 +#, python-format +msgid "%s %s" +msgstr "%s %s" + +#: sphinx/domains/c.py:51 +#: sphinx/domains/python.py:49 +msgid "Parameters" +msgstr "Parametrar" + +#: sphinx/domains/c.py:54 +#: sphinx/domains/javascript.py:137 +#: sphinx/domains/python.py:59 +msgid "Returns" +msgstr "Returnerar" + +#: sphinx/domains/c.py:56 +#: sphinx/domains/python.py:61 +msgid "Return type" +msgstr "Returtyp" + +#: sphinx/domains/c.py:133 +#, python-format +msgid "%s (C function)" +msgstr "%s (C funktion)" + +#: sphinx/domains/c.py:135 +#, python-format +msgid "%s (C member)" +msgstr "%s (C medlem)" + +#: sphinx/domains/c.py:137 +#, python-format +msgid "%s (C macro)" +msgstr "%s (C makro)" + +#: sphinx/domains/c.py:139 +#, python-format +msgid "%s (C type)" +msgstr "%s (C typ)" + +#: sphinx/domains/c.py:141 +#, python-format +msgid "%s (C variable)" +msgstr "%s (C variabel)" + +#: sphinx/domains/c.py:171 +#: sphinx/domains/cpp.py:1031 +#: sphinx/domains/javascript.py:166 +#: sphinx/domains/python.py:497 +msgid "function" +msgstr "funktion" + +#: sphinx/domains/c.py:172 +#: sphinx/domains/cpp.py:1032 +msgid "member" +msgstr "medlem" + +#: sphinx/domains/c.py:173 +msgid "macro" +msgstr "makro" + +#: sphinx/domains/c.py:174 +#: sphinx/domains/cpp.py:1033 +msgid "type" +msgstr "typ" + +#: sphinx/domains/c.py:175 +msgid "variable" +msgstr "variabel" + +#: sphinx/domains/cpp.py:876 +#, python-format +msgid "%s (C++ class)" +msgstr "%s (C++ klass)" + +#: sphinx/domains/cpp.py:891 +#, python-format +msgid "%s (C++ type)" +msgstr "%s (C++ typ)" + +#: sphinx/domains/cpp.py:910 +#, python-format +msgid "%s (C++ member)" +msgstr "%s (C++ medlem)" + +#: sphinx/domains/cpp.py:962 +#, python-format +msgid "%s (C++ function)" +msgstr "%s (C++ funktion)" + +#: sphinx/domains/cpp.py:1030 +#: sphinx/domains/python.py:499 +msgid "class" +msgstr "klass" + +#: sphinx/domains/javascript.py:117 +#: sphinx/domains/python.py:221 +#, python-format +msgid "%s() (built-in function)" +msgstr "%s() (inbyggd funktion)" + +#: sphinx/domains/javascript.py:118 +#: sphinx/domains/python.py:285 +#, python-format +msgid "%s() (%s method)" +msgstr "%s() (%s metod)" + +#: sphinx/domains/javascript.py:120 +#, python-format +msgid "%s (global variable or constant)" +msgstr "%s (global variabel eller konstant)" + +#: sphinx/domains/javascript.py:122 +#: sphinx/domains/python.py:323 +#, python-format +msgid "%s (%s attribute)" +msgstr "%s (%s attribut)" + +#: sphinx/domains/javascript.py:131 +msgid "Arguments" +msgstr "Argument" + +#: sphinx/domains/javascript.py:134 +msgid "Throws" +msgstr "Kastar" + +#: sphinx/domains/javascript.py:167 +#: sphinx/domains/python.py:498 +msgid "data" +msgstr "data" + +#: sphinx/domains/javascript.py:168 +#: sphinx/domains/python.py:504 +msgid "attribute" +msgstr "attribut" + +#: sphinx/domains/python.py:53 +msgid "Variables" +msgstr "Variabler" + +#: sphinx/domains/python.py:56 +msgid "Raises" +msgstr "Väcker" + +#: sphinx/domains/python.py:222 +#: sphinx/domains/python.py:279 +#: sphinx/domains/python.py:291 +#: sphinx/domains/python.py:304 +#, python-format +msgid "%s() (in module %s)" +msgstr "%s() (i modul %s)" + +#: sphinx/domains/python.py:225 +#, python-format +msgid "%s (built-in variable)" +msgstr "%s (inbyggd variabel)" + +#: sphinx/domains/python.py:226 +#: sphinx/domains/python.py:317 +#, python-format +msgid "%s (in module %s)" +msgstr "%s (i modul %s)" + +#: sphinx/domains/python.py:242 +#, python-format +msgid "%s (built-in class)" +msgstr "%s (inbyggd klass)" + +#: sphinx/domains/python.py:243 +#, python-format +msgid "%s (class in %s)" +msgstr "%s (klass i %s)" + +#: sphinx/domains/python.py:283 +#, python-format +msgid "%s() (%s.%s method)" +msgstr "%s() (%s.%s metod)" + +#: sphinx/domains/python.py:295 +#, python-format +msgid "%s() (%s.%s static method)" +msgstr "%s() (%s.%s statisk metod)" + +#: sphinx/domains/python.py:298 +#, python-format +msgid "%s() (%s static method)" +msgstr "%s() (%s statisk metod)" + +#: sphinx/domains/python.py:308 +#, python-format +msgid "%s() (%s.%s class method)" +msgstr "%s() (%s.%s klass metod)" + +#: sphinx/domains/python.py:311 +#, python-format +msgid "%s() (%s class method)" +msgstr "%s() (%s klass metod)" + +#: sphinx/domains/python.py:321 +#, python-format +msgid "%s (%s.%s attribute)" +msgstr "%s (%s.%s attribut)" + +#: sphinx/domains/python.py:366 +msgid "Platforms: " +msgstr "Plattformar:" + +#: sphinx/domains/python.py:372 +#, python-format +msgid "%s (module)" +msgstr "%s (modul)" + +#: sphinx/domains/python.py:429 +msgid "Python Module Index" +msgstr "Python Modul Index" + +#: sphinx/domains/python.py:430 +msgid "modules" +msgstr "moduler" + +#: sphinx/domains/python.py:475 +msgid "Deprecated" +msgstr "Ersatt" + +#: sphinx/domains/python.py:500 +#: sphinx/locale/__init__.py:162 +msgid "exception" +msgstr "undantag" + +#: sphinx/domains/python.py:501 +msgid "method" +msgstr "metod" + +#: sphinx/domains/python.py:502 +msgid "class method" +msgstr "klassmetod" + +#: sphinx/domains/python.py:503 +msgid "static method" +msgstr "statisk metod" + +#: sphinx/domains/python.py:505 +#: sphinx/locale/__init__.py:158 +msgid "module" +msgstr "modul" + +#: sphinx/domains/rst.py:53 +#, python-format +msgid "%s (directive)" +msgstr "%s (direktiv)" + +#: sphinx/domains/rst.py:55 +#, python-format +msgid "%s (role)" +msgstr "%s (roll)" + +#: sphinx/domains/rst.py:103 +msgid "directive" +msgstr "direktiv" + +#: sphinx/domains/rst.py:104 +msgid "role" +msgstr "roll" + +#: sphinx/domains/std.py:68 +#: sphinx/domains/std.py:84 +#, python-format +msgid "environment variable; %s" +msgstr "miljövariabel; %s" + +#: sphinx/domains/std.py:160 +#, python-format +msgid "%scommand line option; %s" +msgstr "%skommandorad växel; %s" + +#: sphinx/domains/std.py:328 +msgid "glossary term" +msgstr "ordlista" + +#: sphinx/domains/std.py:329 +msgid "grammar token" +msgstr "grammatisk token" + +#: sphinx/domains/std.py:330 +msgid "reference label" +msgstr "referensetikett" + +#: sphinx/domains/std.py:331 +msgid "environment variable" +msgstr "miljövariabel" + +#: sphinx/domains/std.py:332 +msgid "program option" +msgstr "programväxel" + +#: sphinx/domains/std.py:360 +#: sphinx/themes/basic/genindex-single.html:11 +#: sphinx/themes/basic/genindex-split.html:11 +#: sphinx/themes/basic/genindex-split.html:14 +#: sphinx/themes/basic/genindex.html:11 +#: sphinx/themes/basic/genindex.html:14 +#: sphinx/themes/basic/genindex.html:50 +#: sphinx/themes/basic/layout.html:125 +#: sphinx/writers/latex.py:173 +msgid "Index" +msgstr "Index" + +#: sphinx/domains/std.py:361 +msgid "Module Index" +msgstr "Modul Index" + +#: sphinx/domains/std.py:362 +#: sphinx/themes/basic/defindex.html:25 +msgid "Search Page" +msgstr "Söksida" + +#: sphinx/ext/autodoc.py:917 +#, python-format +msgid " Bases: %s" +msgstr " Baserad: %s" + +#: sphinx/ext/autodoc.py:950 +#, python-format +msgid "alias of :class:`%s`" +msgstr "alias av :klass:`%s`" + +#: sphinx/ext/todo.py:41 +msgid "Todo" +msgstr "Att göra" + +#: sphinx/ext/todo.py:109 +#, python-format +msgid "(The <<original entry>> is located in %s, line %d.)" +msgstr "(<<Ursprunget>> finns i %s, på rad %d.)" + +#: sphinx/ext/todo.py:117 +msgid "original entry" +msgstr "ursprungsvärde" + +#: sphinx/ext/viewcode.py:66 +msgid "[source]" +msgstr "[source]" + +#: sphinx/ext/viewcode.py:109 +msgid "[docs]" +msgstr "[docs]" + +#: sphinx/ext/viewcode.py:123 +msgid "Module code" +msgstr "Modulkällkod" + +#: sphinx/ext/viewcode.py:129 +#, python-format +msgid "<h1>Source code for %s</h1>" +msgstr "<h1>Källkod för %s</h1>" + +#: sphinx/ext/viewcode.py:156 +msgid "Overview: module code" +msgstr "Översikt: modulkällkod" + +#: sphinx/ext/viewcode.py:157 +msgid "<h1>All modules for which code is available</h1>" +msgstr "<h1>Alla moduler där källkod finns</h1>" + +#: sphinx/locale/__init__.py:139 +msgid "Attention" +msgstr "Observera" + +#: sphinx/locale/__init__.py:140 +msgid "Caution" +msgstr "Varning" + +#: sphinx/locale/__init__.py:141 +msgid "Danger" +msgstr "Risk" + +#: sphinx/locale/__init__.py:142 +msgid "Error" +msgstr "Fel" + +#: sphinx/locale/__init__.py:143 +msgid "Hint" +msgstr "Råd" + +#: sphinx/locale/__init__.py:144 +msgid "Important" +msgstr "Viktigt" + +#: sphinx/locale/__init__.py:145 +msgid "Note" +msgstr "Observera" + +#: sphinx/locale/__init__.py:146 +msgid "See Also" +msgstr "Visa även" + +#: sphinx/locale/__init__.py:147 +msgid "Tip" +msgstr "Tips" + +#: sphinx/locale/__init__.py:148 +msgid "Warning" +msgstr "Varning" + +#: sphinx/locale/__init__.py:152 +#, python-format +msgid "New in version %s" +msgstr "Nyheter i version %s" + +#: sphinx/locale/__init__.py:153 +#, python-format +msgid "Changed in version %s" +msgstr "Förändrat i version %s" + +#: sphinx/locale/__init__.py:154 +#, python-format +msgid "Deprecated since version %s" +msgstr "Ersatt sedan version %s" + +#: sphinx/locale/__init__.py:159 +msgid "keyword" +msgstr "nyckelord" + +#: sphinx/locale/__init__.py:160 +msgid "operator" +msgstr "operator" + +#: sphinx/locale/__init__.py:161 +msgid "object" +msgstr "objekt" + +#: sphinx/locale/__init__.py:163 +msgid "statement" +msgstr "uttryck" + +#: sphinx/locale/__init__.py:164 +msgid "built-in function" +msgstr "inbyggda funktioner" + +#: sphinx/themes/agogo/layout.html:45 +#: sphinx/themes/basic/globaltoc.html:10 +#: sphinx/themes/basic/localtoc.html:11 +msgid "Table Of Contents" +msgstr "Innehållsförteckning" + +#: sphinx/themes/agogo/layout.html:49 +#: sphinx/themes/basic/layout.html:128 +#: sphinx/themes/basic/search.html:11 +#: sphinx/themes/basic/search.html:14 +msgid "Search" +msgstr "Sök" + +#: sphinx/themes/agogo/layout.html:52 +#: sphinx/themes/basic/searchbox.html:15 +msgid "Go" +msgstr "Gå" + +#: sphinx/themes/agogo/layout.html:57 +#: sphinx/themes/basic/searchbox.html:20 +msgid "Enter search terms or a module, class or function name." +msgstr "Ange sökord eller modul-, klass- eller funktionsnamn." + +#: sphinx/themes/agogo/layout.html:78 +#: sphinx/themes/basic/sourcelink.html:14 +msgid "Show Source" +msgstr "Visa källfil" + +#: sphinx/themes/basic/defindex.html:11 +msgid "Overview" +msgstr "Översikt" + +#: sphinx/themes/basic/defindex.html:20 +msgid "Indices and tables:" +msgstr "Index och tabeller" + +#: sphinx/themes/basic/defindex.html:23 +msgid "Complete Table of Contents" +msgstr "Komplett Innehållsförteckning" + +#: sphinx/themes/basic/defindex.html:24 +msgid "lists all sections and subsections" +msgstr "lista över alla paragrafer och underparagrafer" + +#: sphinx/themes/basic/defindex.html:26 +msgid "search this documentation" +msgstr "sök i det här dokumentet" + +#: sphinx/themes/basic/defindex.html:28 +msgid "Global Module Index" +msgstr "Global Modulindex" + +#: sphinx/themes/basic/defindex.html:29 +msgid "quick access to all modules" +msgstr "genväg till alla moduler" + +#: sphinx/themes/basic/defindex.html:31 +msgid "all functions, classes, terms" +msgstr "alla funktioner, klasser, villkor" + +#: sphinx/themes/basic/genindex-single.html:14 +#, python-format +msgid "Index – %(key)s" +msgstr "Index – %(key)s" + +#: sphinx/themes/basic/genindex-single.html:46 +#: sphinx/themes/basic/genindex-split.html:24 +#: sphinx/themes/basic/genindex-split.html:38 +#: sphinx/themes/basic/genindex.html:56 +msgid "Full index on one page" +msgstr "Hela innehållsförteckningen på en sida" + +#: sphinx/themes/basic/genindex-split.html:16 +msgid "Index pages by letter" +msgstr "Innehållsförteckning per inledande bokstav" + +#: sphinx/themes/basic/genindex-split.html:25 +msgid "can be huge" +msgstr "kan bli stort" + +#: sphinx/themes/basic/layout.html:23 +msgid "Navigation" +msgstr "Navigation" + +#: sphinx/themes/basic/layout.html:113 +#, python-format +msgid "Search within %(docstitle)s" +msgstr "Sök bland %(docstitle)s" + +#: sphinx/themes/basic/layout.html:122 +msgid "About these documents" +msgstr "Om dessa dokument" + +#: sphinx/themes/basic/layout.html:131 +msgid "Copyright" +msgstr "Copyright" + +#: sphinx/themes/basic/layout.html:180 +#, python-format +msgid "© <a href=\"%(path)s\">Copyright</a> %(copyright)s." +msgstr "© <a href=\"%(path)s\">Copyright</a> %(copyright)s." + +#: sphinx/themes/basic/layout.html:182 +#, python-format +msgid "© Copyright %(copyright)s." +msgstr "© Copyright %(copyright)s." + +#: sphinx/themes/basic/layout.html:186 +#, python-format +msgid "Last updated on %(last_updated)s." +msgstr "Senast uppdaterad %(last_updated)s." + +#: sphinx/themes/basic/layout.html:189 +#, python-format +msgid "Created using <a href=\"http://sphinx.pocoo.org/\">Sphinx</a> %(sphinx_version)s." +msgstr "Skapad med <a href=\"http://sphinx.pocoo.org/\">Sphinx</a> %(sphinx_version)s." + +#: sphinx/themes/basic/opensearch.xml:4 +#, python-format +msgid "Search %(docstitle)s" +msgstr "Sök %(docstitle)s" + +#: sphinx/themes/basic/relations.html:11 +msgid "Previous topic" +msgstr "Föregående titel" + +#: sphinx/themes/basic/relations.html:13 +msgid "previous chapter" +msgstr "Föregående kapitel" + +#: sphinx/themes/basic/relations.html:16 +msgid "Next topic" +msgstr "Nästa titel" + +#: sphinx/themes/basic/relations.html:18 +msgid "next chapter" +msgstr "Nästa kapitel" + +#: sphinx/themes/basic/search.html:18 +msgid "" +"Please activate JavaScript to enable the search\n" +" functionality." +msgstr "Var god aktivera JavaScript för sökfunktionalitet." + +#: sphinx/themes/basic/search.html:23 +msgid "" +"From here you can search these documents. Enter your search\n" +" words into the box below and click \"search\". Note that the search\n" +" function will automatically search for all of the words. Pages\n" +" containing fewer words won't appear in the result list." +msgstr "" +"Här kan du söka bland dessa dokument. Ange sökord nedan och klicka \"sök\".\n" +" Sökningen måste träffa på samtliga angivna sökord." + +#: sphinx/themes/basic/search.html:30 +msgid "search" +msgstr "sök" + +#: sphinx/themes/basic/search.html:34 +#: sphinx/themes/basic/static/searchtools.js:489 +msgid "Search Results" +msgstr "Sökresultat" + +#: sphinx/themes/basic/search.html:36 +msgid "Your search did not match any results." +msgstr "Din sökning gav inga resultat." + +#: sphinx/themes/basic/searchbox.html:12 +msgid "Quick search" +msgstr "Snabbsök" + +#: sphinx/themes/basic/sourcelink.html:11 +msgid "This Page" +msgstr "Denna Sida" + +#: sphinx/themes/basic/changes/frameset.html:5 +#: sphinx/themes/basic/changes/versionchanges.html:12 +#, python-format +msgid "Changes in Version %(version)s — %(docstitle)s" +msgstr "Förändringar i Version %(version)s — %(docstitle)s" + +#: sphinx/themes/basic/changes/rstsource.html:5 +#, python-format +msgid "%(filename)s — %(docstitle)s" +msgstr "%(filename)s — %(docstitle)s" + +#: sphinx/themes/basic/changes/versionchanges.html:17 +#, python-format +msgid "Automatically generated list of changes in version %(version)s" +msgstr "Automatiskt genererad lista över förändringar i version %(version)s" + +#: sphinx/themes/basic/changes/versionchanges.html:18 +msgid "Library changes" +msgstr "Förändringar i bibliotek" + +#: sphinx/themes/basic/changes/versionchanges.html:23 +msgid "C API changes" +msgstr "Förändringar i C API" + +#: sphinx/themes/basic/changes/versionchanges.html:25 +msgid "Other changes" +msgstr "Övriga förändringar" + +#: sphinx/themes/basic/static/doctools.js:154 +#: sphinx/writers/html.py:482 +#: sphinx/writers/html.py:487 +msgid "Permalink to this headline" +msgstr "Permalink till denna rubrik" + +#: sphinx/themes/basic/static/doctools.js:160 +#: sphinx/writers/html.py:87 +msgid "Permalink to this definition" +msgstr "Permalink till denna definition" + +#: sphinx/themes/basic/static/doctools.js:189 +msgid "Hide Search Matches" +msgstr "Dölj Sökresultat" + +#: sphinx/themes/basic/static/searchtools.js:285 +msgid "Searching" +msgstr "Söker" + +#: sphinx/themes/basic/static/searchtools.js:290 +msgid "Preparing search..." +msgstr "Förbereder sökning..." + +#: sphinx/themes/basic/static/searchtools.js:364 +msgid ", in " +msgstr ", i " + +#: sphinx/themes/basic/static/searchtools.js:491 +msgid "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." +msgstr "Din sökning gav inga resultat. Kolla stavning och att du valt tillräckligt med kategorier." + +#: sphinx/themes/basic/static/searchtools.js:493 +#, python-format +msgid "Search finished, found %s page(s) matching the search query." +msgstr "Sökning färdig, hittade %s träffar." + +#: sphinx/themes/default/static/sidebar.js:66 +msgid "Expand sidebar" +msgstr "Expandera sidolist" + +#: sphinx/themes/default/static/sidebar.js:79 +#: sphinx/themes/default/static/sidebar.js:106 +msgid "Collapse sidebar" +msgstr "Dölj sidolist" + +#: sphinx/themes/haiku/layout.html:26 +msgid "Contents" +msgstr "Innehåll" + +#: sphinx/writers/latex.py:171 +msgid "Release" +msgstr "Utgåva" + +#: sphinx/writers/latex.py:572 +#: sphinx/writers/manpage.py:178 +msgid "Footnotes" +msgstr "Fotnoter" + +#: sphinx/writers/latex.py:641 +msgid "continued from previous page" +msgstr "fortsättning från föregående sida" + +#: sphinx/writers/latex.py:646 +msgid "Continued on next page" +msgstr "Fortsätter på nästa sida" + +#: sphinx/writers/text.py:422 +msgid "[image]" +msgstr "[image]" + -- cgit v1.2.1 From d91c69f485314bf1db5d81e2716b1097995557c5 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Wed, 25 Aug 2010 10:46:49 +0000 Subject: Review of Swedish translation by Ludvig Ericson. --- sphinx/locale/sv/LC_MESSAGES/sphinx.mo | Bin 9511 -> 9509 bytes sphinx/locale/sv/LC_MESSAGES/sphinx.po | 43 ++++++++++++++++++--------------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/sphinx/locale/sv/LC_MESSAGES/sphinx.mo b/sphinx/locale/sv/LC_MESSAGES/sphinx.mo index 203ead14..8cf76751 100644 Binary files a/sphinx/locale/sv/LC_MESSAGES/sphinx.mo and b/sphinx/locale/sv/LC_MESSAGES/sphinx.mo differ diff --git a/sphinx/locale/sv/LC_MESSAGES/sphinx.po b/sphinx/locale/sv/LC_MESSAGES/sphinx.po index f5f3b7e4..f449e8f6 100644 --- a/sphinx/locale/sv/LC_MESSAGES/sphinx.po +++ b/sphinx/locale/sv/LC_MESSAGES/sphinx.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2010-05-24 23:53+0200\n" -"PO-Revision-Date: \n" +"PO-Revision-Date: 2010-08-25 12:36+0200\n" "Last-Translator: Henrik Holmboe <henrik@holmboe.se>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -11,7 +11,10 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Swedish\n" "X-Poedit-Country: SWEDEN\n" -"X-Poedit-Basepath: /home/hh/Local/src/sphinx\n" + +# Translators (rev. chron. order): +# Ludvig Ericson <ludvig@lericson.se> +# Henrik Holmboe <henrik@holmboe.se> #: sphinx/environment.py:106 #: sphinx/writers/latex.py:184 @@ -77,7 +80,7 @@ msgstr "Upphovsman:" #: sphinx/directives/other.py:238 msgid "See also" -msgstr "Visa även" +msgstr "Se även" #: sphinx/domains/__init__.py:253 #, python-format @@ -103,27 +106,27 @@ msgstr "Returtyp" #: sphinx/domains/c.py:133 #, python-format msgid "%s (C function)" -msgstr "%s (C funktion)" +msgstr "%s (C-funktion)" #: sphinx/domains/c.py:135 #, python-format msgid "%s (C member)" -msgstr "%s (C medlem)" +msgstr "%s (C-medlem)" #: sphinx/domains/c.py:137 #, python-format msgid "%s (C macro)" -msgstr "%s (C makro)" +msgstr "%s (C-makro)" #: sphinx/domains/c.py:139 #, python-format msgid "%s (C type)" -msgstr "%s (C typ)" +msgstr "%s (C-typ)" #: sphinx/domains/c.py:141 #, python-format msgid "%s (C variable)" -msgstr "%s (C variabel)" +msgstr "%s (C-variabel)" #: sphinx/domains/c.py:171 #: sphinx/domains/cpp.py:1031 @@ -153,22 +156,22 @@ msgstr "variabel" #: sphinx/domains/cpp.py:876 #, python-format msgid "%s (C++ class)" -msgstr "%s (C++ klass)" +msgstr "%s (C++-klass)" #: sphinx/domains/cpp.py:891 #, python-format msgid "%s (C++ type)" -msgstr "%s (C++ typ)" +msgstr "%s (C++-typ)" #: sphinx/domains/cpp.py:910 #, python-format msgid "%s (C++ member)" -msgstr "%s (C++ medlem)" +msgstr "%s (C++-medlem)" #: sphinx/domains/cpp.py:962 #, python-format msgid "%s (C++ function)" -msgstr "%s (C++ funktion)" +msgstr "%s (C++-funktion)" #: sphinx/domains/cpp.py:1030 #: sphinx/domains/python.py:499 @@ -271,12 +274,12 @@ msgstr "%s() (%s statisk metod)" #: sphinx/domains/python.py:308 #, python-format msgid "%s() (%s.%s class method)" -msgstr "%s() (%s.%s klass metod)" +msgstr "%s() (%s.%s klassmetod)" #: sphinx/domains/python.py:311 #, python-format msgid "%s() (%s class method)" -msgstr "%s() (%s klass metod)" +msgstr "%s() (%s klassmetod)" #: sphinx/domains/python.py:321 #, python-format @@ -294,7 +297,7 @@ msgstr "%s (modul)" #: sphinx/domains/python.py:429 msgid "Python Module Index" -msgstr "Python Modul Index" +msgstr "Python Modulindex" #: sphinx/domains/python.py:430 msgid "modules" @@ -389,7 +392,7 @@ msgstr "Index" #: sphinx/domains/std.py:361 msgid "Module Index" -msgstr "Modul Index" +msgstr "Modulindex" #: sphinx/domains/std.py:362 #: sphinx/themes/basic/defindex.html:25 @@ -404,7 +407,7 @@ msgstr " Baserad: %s" #: sphinx/ext/autodoc.py:950 #, python-format msgid "alias of :class:`%s`" -msgstr "alias av :klass:`%s`" +msgstr "alias för :class:`%s`" #: sphinx/ext/todo.py:41 msgid "Todo" @@ -446,7 +449,7 @@ msgstr "<h1>Alla moduler där källkod finns</h1>" #: sphinx/locale/__init__.py:139 msgid "Attention" -msgstr "Observera" +msgstr "Uppmärksamma" #: sphinx/locale/__init__.py:140 msgid "Caution" @@ -474,7 +477,7 @@ msgstr "Observera" #: sphinx/locale/__init__.py:146 msgid "See Also" -msgstr "Visa även" +msgstr "Se även" #: sphinx/locale/__init__.py:147 msgid "Tip" @@ -716,7 +719,7 @@ msgstr "Förändringar i bibliotek" #: sphinx/themes/basic/changes/versionchanges.html:23 msgid "C API changes" -msgstr "Förändringar i C API" +msgstr "Förändringar i C-API" #: sphinx/themes/basic/changes/versionchanges.html:25 msgid "Other changes" -- cgit v1.2.1 From a571d75ba0f9ebeb317e5fcb7527030554908170 Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Wed, 25 Aug 2010 11:33:25 +0000 Subject: Fix test. --- tests/test_build_html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_build_html.py b/tests/test_build_html.py index c75e7c2d..8f3a8f2a 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -38,7 +38,7 @@ http://www.python.org/logo.png %(root)s/includes.txt:\\d*: \\(WARNING/2\\) Encoding 'utf-8-sig' used for \ reading included file u'.*?wrongenc.inc' seems to be wrong, try giving an \ :encoding: option\\n? -%(root)s/includes.txt:4: WARNING: download file not readable: nonexisting.png +%(root)s/includes.txt:4: WARNING: download file not readable: .*?nonexisting.png %(root)s/objects.txt:\\d*: WARNING: using old C markup; please migrate to \ new-style markup \(e.g. c:function instead of cfunction\), see \ http://sphinx.pocoo.org/domains.html -- cgit v1.2.1 From 2a188f163fd6e10eec3b7c45e6a44c1d1f3ebf6c Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Wed, 25 Aug 2010 11:33:30 +0000 Subject: #504: Add an ``index`` role, to make inline index entries. --- CHANGES | 2 ++ doc/markup/inline.rst | 1 + doc/markup/misc.rst | 79 ++++++++++++++++++++++++++++++++++++++++++++++ doc/markup/para.rst | 62 ------------------------------------ sphinx/directives/other.py | 31 ++---------------- sphinx/roles.py | 24 +++++++++++++- sphinx/util/nodes.py | 32 +++++++++++++++++++ tests/root/markup.txt | 2 ++ 8 files changed, 142 insertions(+), 91 deletions(-) diff --git a/CHANGES b/CHANGES index 45467a16..0ea9a348 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,8 @@ Release 1.1 (in development) * #460: Allow limiting the depth of section numbers for HTML. +* #504: Add an ``index`` role, to make inline index entries. + * #443: Allow referencing external graphviz files. * #221: Add Swedish locale. diff --git a/doc/markup/inline.rst b/doc/markup/inline.rst index 35981edc..78aaea69 100644 --- a/doc/markup/inline.rst +++ b/doc/markup/inline.rst @@ -309,6 +309,7 @@ in a different style: If you don't need the "variable part" indication, use the standard ````code```` instead. +There is also an :rst:role:`index` role to generate index entries. The following roles generate external links: diff --git a/doc/markup/misc.rst b/doc/markup/misc.rst index 6173589b..44da3aac 100644 --- a/doc/markup/misc.rst +++ b/doc/markup/misc.rst @@ -62,6 +62,85 @@ Meta-information markup :confval:`show_authors` configuration value is True. +Index-generating markup +----------------------- + +Sphinx automatically creates index entries from all object descriptions (like +functions, classes or attributes) like discussed in :ref:`domains`. + +However, there is also explicit markup available, to make the index more +comprehensive and enable index entries in documents where information is not +mainly contained in information units, such as the language reference. + +.. rst:directive:: .. index:: <entries> + + This directive contains one or more index entries. Each entry consists of a + type and a value, separated by a colon. + + For example:: + + .. index:: + single: execution; context + module: __main__ + module: sys + triple: module; search; path + + The execution context + --------------------- + + ... + + This directive contains five entries, which will be converted to entries in + the generated index which link to the exact location of the index statement + (or, in case of offline media, the corresponding page number). + + Since index directives generate cross-reference targets at their location in + the source, it makes sense to put them *before* the thing they refer to -- + e.g. a heading, as in the example above. + + The possible entry types are: + + single + Creates a single index entry. Can be made a subentry by separating the + subentry text with a semicolon (this notation is also used below to + describe what entries are created). + pair + ``pair: loop; statement`` is a shortcut that creates two index entries, + namely ``loop; statement`` and ``statement; loop``. + triple + Likewise, ``triple: module; search; path`` is a shortcut that creates + three index entries, which are ``module; search path``, ``search; path, + module`` and ``path; module search``. + module, keyword, operator, object, exception, statement, builtin + These all create two index entries. For example, ``module: hashlib`` + creates the entries ``module; hashlib`` and ``hashlib; module``. (These + are Python-specific and therefore deprecated.) + + For index directives containing only "single" entries, there is a shorthand + notation:: + + .. index:: BNF, grammar, syntax, notation + + This creates four index entries. + +.. rst:role:: index + + While the :rst:dir:`index` directive is a block-level markup and links to the + beginning of the next paragraph, there is also a corresponding role that sets + the link target directly where it is used. + + The content of the role can be a simple phrase, which is then kept in the + text and used as an index entry. It can also be a combination of text and + index entry, styled like with explicit targets of cross-references. In that + case, the "target" part can be a full entry as described for the directive + above. For example:: + + This is a normal reST :index:`paragraph` that contains several + :index:`index entries <pair: index; entry>`. + + .. versionadded:: 1.1 + + .. _tags: Including content based on tags diff --git a/doc/markup/para.rst b/doc/markup/para.rst index ecc6b4a6..52a5019b 100644 --- a/doc/markup/para.rst +++ b/doc/markup/para.rst @@ -144,68 +144,6 @@ For local tables of contents, use the standard reST :dudir:`contents directive <contents>`. -Index-generating markup ------------------------ - -Sphinx automatically creates index entries from all object descriptions (like -functions, classes or attributes) like discussed in :ref:`domains`. - -However, there is also an explicit directive available, to make the index more -comprehensive and enable index entries in documents where information is not -mainly contained in information units, such as the language reference. - -.. rst:directive:: .. index:: <entries> - - This directive contains one or more index entries. Each entry consists of a - type and a value, separated by a colon. - - For example:: - - .. index:: - single: execution; context - module: __main__ - module: sys - triple: module; search; path - - The execution context - --------------------- - - ... - - This directive contains five entries, which will be converted to entries in - the generated index which link to the exact location of the index statement - (or, in case of offline media, the corresponding page number). - - Since index directives generate cross-reference targets at their location in - the source, it makes sense to put them *before* the thing they refer to -- - e.g. a heading, as in the example above. - - The possible entry types are: - - single - Creates a single index entry. Can be made a subentry by separating the - subentry text with a semicolon (this notation is also used below to - describe what entries are created). - pair - ``pair: loop; statement`` is a shortcut that creates two index entries, - namely ``loop; statement`` and ``statement; loop``. - triple - Likewise, ``triple: module; search; path`` is a shortcut that creates - three index entries, which are ``module; search path``, ``search; path, - module`` and ``path; module search``. - module, keyword, operator, object, exception, statement, builtin - These all create two index entries. For example, ``module: hashlib`` - creates the entries ``module; hashlib`` and ``hashlib; module``. (These - are Python-specific and therefore deprecated.) - - For index directives containing only "single" entries, there is a shorthand - notation:: - - .. index:: BNF, grammar, syntax, notation - - This creates four index entries. - - Glossary -------- diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index b5252e86..cbf19b55 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -13,9 +13,9 @@ from docutils import nodes from docutils.parsers.rst import Directive, directives from sphinx import addnodes -from sphinx.locale import pairindextypes, _ +from sphinx.locale import _ from sphinx.util import url_re, docname_join -from sphinx.util.nodes import explicit_title_re +from sphinx.util.nodes import explicit_title_re, process_index_entry from sphinx.util.compat import make_admonition from sphinx.util.matching import patfilter @@ -157,10 +157,6 @@ class Index(Directive): final_argument_whitespace = True option_spec = {} - indextypes = [ - 'single', 'pair', 'double', 'triple', - ] - def run(self): arguments = self.arguments[0].split('\n') env = self.state.document.settings.env @@ -170,28 +166,7 @@ class Index(Directive): indexnode = addnodes.index() indexnode['entries'] = ne = [] for entry in arguments: - entry = entry.strip() - for type in pairindextypes: - if entry.startswith(type+':'): - value = entry[len(type)+1:].strip() - value = pairindextypes[type] + '; ' + value - ne.append(('pair', value, targetid, value)) - break - else: - for type in self.indextypes: - if entry.startswith(type+':'): - value = entry[len(type)+1:].strip() - if type == 'double': - type = 'pair' - ne.append((type, value, targetid, value)) - break - # shorthand notation for single entries - else: - for value in entry.split(','): - value = value.strip() - if not value: - continue - ne.append(('single', value, targetid, value)) + ne.extend(process_index_entry(entry, targetid)) return [indexnode, targetnode] diff --git a/sphinx/roles.py b/sphinx/roles.py index f08c3f00..b44868e6 100644 --- a/sphinx/roles.py +++ b/sphinx/roles.py @@ -18,7 +18,7 @@ from docutils.parsers.rst import roles from sphinx import addnodes from sphinx.locale import _ from sphinx.util import ws_re -from sphinx.util.nodes import split_explicit_title +from sphinx.util.nodes import split_explicit_title, process_index_entry generic_docroles = { @@ -268,6 +268,27 @@ def abbr_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): return [addnodes.abbreviation(abbr, abbr, explanation=expl)], [] +def index_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): + # create new reference target + env = inliner.document.settings.env + targetid = 'index-%s' % env.new_serialno('index') + targetnode = nodes.target('', '', ids=[targetid]) + # split text and target in role content + has_explicit_title, title, target = split_explicit_title(text) + title = utils.unescape(title) + target = utils.unescape(target) + # if an explicit target is given, we can process it as a full entry + if has_explicit_title: + entries = process_index_entry(target, targetid) + # otherwise we just create a "single" entry + else: + entries = [('single', target, targetid, target)] + indexnode = addnodes.index() + indexnode['entries'] = entries + textnode = nodes.Text(title, title) + return [indexnode, targetnode, textnode], [] + + specific_docroles = { # links to download references 'download': XRefRole(nodeclass=addnodes.download_reference), @@ -281,6 +302,7 @@ specific_docroles = { 'file': emph_literal_role, 'samp': emph_literal_role, 'abbr': abbr_role, + 'index': index_role, } for rolename, func in specific_docroles.iteritems(): diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 2e383b0a..adce565c 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -14,6 +14,7 @@ import re from docutils import nodes from sphinx import addnodes +from sphinx.locale import pairindextypes from sphinx.util.pycompat import class_types @@ -72,6 +73,37 @@ def split_explicit_title(text): return False, text, text +indextypes = [ + 'single', 'pair', 'double', 'triple', +] + +def process_index_entry(entry, targetid): + indexentries = [] + entry = entry.strip() + for type in pairindextypes: + if entry.startswith(type+':'): + value = entry[len(type)+1:].strip() + value = pairindextypes[type] + '; ' + value + indexentries.append(('pair', value, targetid, value)) + break + else: + for type in indextypes: + if entry.startswith(type+':'): + value = entry[len(type)+1:].strip() + if type == 'double': + type = 'pair' + indexentries.append((type, value, targetid, value)) + break + # shorthand notation for single entries + else: + for value in entry.split(','): + value = value.strip() + if not value: + continue + indexentries.append(('single', value, targetid, value)) + return indexentries + + def inline_all_toctrees(builder, docnameset, docname, tree, colorfunc): """Inline all toctrees in the *tree*. diff --git a/tests/root/markup.txt b/tests/root/markup.txt index a72285ed..84d9581a 100644 --- a/tests/root/markup.txt +++ b/tests/root/markup.txt @@ -132,6 +132,8 @@ Adding \n to test unescaping. Test :abbr:`abbr (abbreviation)` and another :abbr:`abbr (abbreviation)`. +Testing the :index:`index` role, also available with +:index:`explicit <pair: title; explicit>` title. .. _with: -- cgit v1.2.1 From 1bd4ac3103fd8a4944376a766422ba4a0a7416bd Mon Sep 17 00:00:00 2001 From: Georg Brandl <georg@python.org> Date: Wed, 25 Aug 2010 11:52:41 +0000 Subject: Give a better issue number. --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 0ea9a348..3833a25b 100644 --- a/CHANGES +++ b/CHANGES @@ -10,7 +10,7 @@ Release 1.1 (in development) * #460: Allow limiting the depth of section numbers for HTML. -* #504: Add an ``index`` role, to make inline index entries. +* #138: Add an ``index`` role, to make inline index entries. * #443: Allow referencing external graphviz files. -- cgit v1.2.1