diff options
author | zax <zach.smith@makespace.com> | 2015-11-01 12:46:54 -0500 |
---|---|---|
committer | Zach Smith <zach.smith@makespace.com> | 2015-11-07 18:45:50 -0500 |
commit | 29e059bccc2206f927d1a3ca348ed04ae2b1a17e (patch) | |
tree | b4f0a7381d6c80834c13a985739a650373f90e39 | |
parent | 1fc58dd74503e3346314a51c9427de1a1228414c (diff) | |
download | pycco-29e059bccc2206f927d1a3ca348ed04ae2b1a17e.tar.gz |
Basic CI with Travis and Coveralls.
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | .travis.yml | 10 | ||||
-rw-r--r-- | README.md (renamed from README) | 7 | ||||
-rw-r--r-- | pycco/main.py | 36 | ||||
-rw-r--r-- | pycco/tests/test_pycco.py | 29 | ||||
-rw-r--r-- | requirements.test.txt | 3 | ||||
-rw-r--r-- | requirements.txt | 1 | ||||
-rw-r--r-- | tests/__init__.py (renamed from pycco/tests/__init__.py) | 0 | ||||
-rw-r--r-- | tests/test_pycco.py | 107 |
9 files changed, 153 insertions, 41 deletions
@@ -1,3 +1,4 @@ +.coverage *.pyc /Pycco.egg-info build/* diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..bfbe563 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: python +python: + - '2.7' +install: + - 'pip install -r requirements.txt' + - 'pip install -r requirements.test.txt' +script: + - 'py.test --cov=pycco tests/' +after_success: + - coveralls @@ -1,3 +1,4 @@ +``` 888888b. 888 Y88b 888 888 @@ -9,6 +10,7 @@ 888 Y8b d88P "Y88P" +``` Pycco is a Python port of Docco: the original quick-and-dirty, hundred-line- long, literate-programming-style documentation generator. For more information, @@ -22,4 +24,7 @@ CoffeeScript (Original) - http://jashkenas.github.com/docco/ Ruby - http://rtomayko.github.com/rocco/ -Sh - http://rtomayko.github.com/shocco/
\ No newline at end of file +Sh - http://rtomayko.github.com/shocco/ + +[![Build Status](https://travis-ci.org/subsetpark/pycco.svg?branch=hypothesis)](https://travis-ci.org/subsetpark/pycco) +[![Coverage Status](https://coveralls.io/repos/subsetpark/pycco/badge.svg?branch=hypothesis&service=github)](https://coveralls.io/github/subsetpark/pycco?branch=hypothesis) diff --git a/pycco/main.py b/pycco/main.py index 6b8abac..20e5a6b 100644 --- a/pycco/main.py +++ b/pycco/main.py @@ -116,7 +116,7 @@ def parse(code, language): elif multi_line: # Remove leading spaces - if re.match(r' {:d}'.format(len(indent_level), line)): + if re.match(r' {:d}'.format(len(indent_level)), line): docs_text += line[len(indent_level):] + '\n' else: docs_text += line + '\n' @@ -300,7 +300,7 @@ languages = { "multistart": "=begin", "multiend": "=end"}, ".py": {"name": "python", "symbol": "#", - "multistart": '"""', "multiend": '"""' }, + "multistart": '"""', "multiend": '"""'}, ".scm": {"name": "scheme", "symbol": ";;", "multistart": "#|", "multiend": "|#"}, @@ -343,15 +343,21 @@ def get_language(source, code, language=None): else: raise ValueError("Unknown forced language: " + language) - m = re.match(r'.*(\..+)', os.path.basename(source)) + m = re.match(r'.*(\..+)', os.path.basename(source)) if source else None if m and m.group(1) in languages: return languages[m.group(1)] else: - lang = lexers.guess_lexer(code).name.lower() - for l in languages.values(): - if l["name"] == lang: - return l - else: + try: + lang = lexers.guess_lexer(code).name.lower() + for l in languages.values(): + if l["name"] == lang: + return l + else: + raise ValueError() + except ValueError: + # If pygments can't find any lexers, it will raise its own + # subclass of ValueError. We will catch it and raise ours + # for consistency. raise ValueError("Can't figure out the language!") @@ -392,11 +398,19 @@ def shift(list, default): def ensure_directory(directory): - """Ensure that the destination directory exists.""" - + """ + Sanitize directory string and ensure that the destination directory exists. + """ + # Sanitization regexp copied from + # http://stackoverflow.com/questions/92438/stripping-non-printable-characters-from-a-string-in-python + control_chars = ''.join(map(unichr, range(0, 32) + range(127, 160))) + control_char_re = re.compile(u'[{}]'.format(re.escape(control_chars))) + directory = control_char_re.sub('', directory) if not os.path.isdir(directory): os.makedirs(directory) + return directory + def template(source): return lambda context: pystache.render(source, context) @@ -426,7 +440,7 @@ def process(sources, preserve_paths=True, outdir=None, language=None): # Proceed to generating the documentation. if sources: - ensure_directory(outdir) + outdir = ensure_directory(outdir) css = open(path.join(outdir, "pycco.css"), "w") css.write(pycco_styles) css.close() diff --git a/pycco/tests/test_pycco.py b/pycco/tests/test_pycco.py deleted file mode 100644 index 43abe36..0000000 --- a/pycco/tests/test_pycco.py +++ /dev/null @@ -1,29 +0,0 @@ -from hypothesis import given -from hypothesis.strategies import lists, text, booleans, integers -import pycco.main as p -import copy - - -@given(lists(text()), text()) -def test_shift(fragments, default): - if fragments == []: - assert p.shift(fragments, default) == default - else: - fragments2 = copy.copy(fragments) - head = p.shift(fragments, default) - assert [head] + fragments == fragments2 - - -@given(text(), booleans(), text(min_size=1)) -def test_destination(filepath, preserve_paths, outdir): - dest = p.destination(filepath, preserve_paths=preserve_paths, outdir=outdir) - assert dest.startswith(outdir) - assert dest.endswith(".html") - - -@given(integers(min_value=0, max_value=12), text()) -def test_parse(n, source): - languages = p.languages - l = languages[languages.keys()[n]] - parsed = p.parse(source, l) - assert [{"code_text", "docs_text"} == set(s.keys()) for s in parsed] diff --git a/requirements.test.txt b/requirements.test.txt new file mode 100644 index 0000000..8439fc2 --- /dev/null +++ b/requirements.test.txt @@ -0,0 +1,3 @@ +hypothesis==1.14.0 +pytest-cov==2.2.0 +coveralls==1.1 diff --git a/requirements.txt b/requirements.txt index b2e7043..38964da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pystache==0.5.4 +Pygments==2.0.2 markdown==2.6.3 diff --git a/pycco/tests/__init__.py b/tests/__init__.py index e69de29..e69de29 100644 --- a/pycco/tests/__init__.py +++ b/tests/__init__.py diff --git a/tests/test_pycco.py b/tests/test_pycco.py new file mode 100644 index 0000000..04cb57e --- /dev/null +++ b/tests/test_pycco.py @@ -0,0 +1,107 @@ +import copy +import tempfile +import pytest +import os +import re +from hypothesis import given, example, assume +from hypothesis.strategies import lists, text, booleans, choices, none + +import pycco.main as p + +PYTHON = p.languages['.py'] +PYCCO_SOURCE = 'pycco/main.py' +FOO_FUNCTION = """def foo():\n return True""" + + +@given(lists(text()), text()) +def test_shift(fragments, default): + if fragments == []: + assert p.shift(fragments, default) == default + else: + fragments2 = copy.copy(fragments) + head = p.shift(fragments, default) + assert [head] + fragments == fragments2 + + +@given(text(), booleans(), text(min_size=1)) +@example("/foo", True, "0") +def test_destination(filepath, preserve_paths, outdir): + dest = p.destination(filepath, preserve_paths=preserve_paths, outdir=outdir) + assert dest.startswith(outdir) + assert dest.endswith(".html") + + +@given(choices(), text()) +def test_parse(choice, source): + l = choice(p.languages.values()) + parsed = p.parse(source, l) + assert [{"code_text", "docs_text"} == set(s.keys()) for s in parsed] + + +def test_skip_coding_directive(): + source = "# -*- coding: utf-8 -*-\n" + FOO_FUNCTION + parsed = p.parse(source, PYTHON) + for section in parsed: + assert "coding" not in section['code_text'] + + +def test_multi_line_leading_spaces(): + source = "# This is a\n# comment that\n# is indented\n" + source += FOO_FUNCTION + parsed = p.parse(source, PYTHON) + # The resulting comment has leading spaces stripped out. + assert parsed[0]["docs_text"] == "This is a\ncomment that\nis indented\n" + + +@given(text(), text()) +def test_get_language_specify_language(source, code): + assert p.get_language(source, code, language="python") == p.languages['.py'] + + with pytest.raises(ValueError): + p.get_language(source, code, language="non-existent") + + +@given(text() | none()) +def test_get_language_bad_source(source): + code = "#!/usr/bin/python\n" + code += FOO_FUNCTION + assert p.get_language(source, code) == PYTHON + with pytest.raises(ValueError) as e: + assert p.get_language(source, "badlang") + + assert e.value.message == "Can't figure out the language!" + + +@given(text() | none()) +def test_get_language_bad_code(code): + source = "test.py" + assert p.get_language(source, code) == PYTHON + + +@given(text(max_size=64)) +def test_ensure_directory(dir_name): + tempdir = os.path.join(tempfile.gettempdir(), dir_name) + + # Copy and paste sanitization from function, but only for housekeeping. We + # pass in the unsanitized string to the function. + control_chars = ''.join(map(unichr, range(0, 32) + range(127, 160))) + control_char_re = re.compile(u'[{}]'.format(re.escape(control_chars))) + safe_name = control_char_re.sub('', tempdir) + + if not os.path.isdir(safe_name): + assume(os.access(safe_name, os.W_OK)) + p.ensure_directory(tempdir) + assert os.path.isdir(safe_name) + +# The following functions get good test coverage, but effort should be put into +# decomposing the functions they test and actually testing their output. + + +def test_generate_documentation(): + p.generate_documentation(PYCCO_SOURCE, outdir=tempfile.gettempdir()) + + +@given(booleans(), choices()) +def test_process(preserve_paths, choice): + lang_name = choice([l["name"] for l in p.languages.values()]) + p.process([PYCCO_SOURCE], preserve_paths=preserve_paths, outdir=tempfile.gettempdir(), language=lang_name) |