summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorzax <zach.smith@makespace.com>2015-11-01 12:46:54 -0500
committerZach Smith <zach.smith@makespace.com>2015-11-07 18:45:50 -0500
commit29e059bccc2206f927d1a3ca348ed04ae2b1a17e (patch)
treeb4f0a7381d6c80834c13a985739a650373f90e39
parent1fc58dd74503e3346314a51c9427de1a1228414c (diff)
downloadpycco-29e059bccc2206f927d1a3ca348ed04ae2b1a17e.tar.gz
Basic CI with Travis and Coveralls.
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml10
-rw-r--r--README.md (renamed from README)7
-rw-r--r--pycco/main.py36
-rw-r--r--pycco/tests/test_pycco.py29
-rw-r--r--requirements.test.txt3
-rw-r--r--requirements.txt1
-rw-r--r--tests/__init__.py (renamed from pycco/tests/__init__.py)0
-rw-r--r--tests/test_pycco.py107
9 files changed, 153 insertions, 41 deletions
diff --git a/.gitignore b/.gitignore
index 5306a68..e1c9655 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/README b/README.md
index 5343728..9cad492 100644
--- a/README
+++ b/README.md
@@ -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)