diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..20caabd --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[report] +omit = + */python?.?/* + *__init__* + tests/* diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..016c117 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,2 @@ +service_name: travis-ci +repo_token: d9edOeB1k2fIKJMosheHF3jpxYJxxpuid diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..d1f7d4e --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +exclude = __init__.py, docs/, build/, setup.py diff --git a/.gitignore b/.gitignore index 3ea8eea..b8b8725 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,5 @@ ENV/ # Rope project settings .ropeproject MANIFEST + +.vscode diff --git a/.travis.yml b/.travis.yml index 8eb5e0d..93d9fee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,3 +23,6 @@ before_script: script: - make test + +after_success: + - coveralls diff --git a/Makefile b/Makefile index 21047aa..39c85f1 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,8 @@ init: pip install -r requirements.txt lint: - flake8 --exclude __init__.py markdown_generator + flake8 test: - nosetests + python -m pytest + coverage run -m pytest -v diff --git a/README.md b/README.md index bfeaf27..beea946 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![Build Status](https://travis-ci.org/cmccandless/markdown-generator.svg?branch=master)](https://travis-ci.org/cmccandless/markdown-generator) +[![Coverage Status](https://coveralls.io/repos/github/cmccandless/markdown-generator/badge.svg?branch=master)](https://coveralls.io/github/cmccandless/markdown-generator?branch=master) Markdown Generator ======================== @@ -6,4 +7,3 @@ Markdown Generator Python module for generator GitHub-flavored markdown pip install markdown-generator - diff --git a/example.md b/example.md index cdeb7f8..7a812bc 100644 --- a/example.md +++ b/example.md @@ -41,7 +41,7 @@ writelines()2 ![my alt text](https://example.com/link/to/image.png) ```Python -s = 'Python syntax highlighting +s = 'Python syntax highlighting' print(s) ``` | col1 | col2 | col3 | diff --git a/example.py b/example.py index 34ab2a2..2e134e4 100644 --- a/example.py +++ b/example.py @@ -46,13 +46,13 @@ writer.writeline(image) code = mg.Code('Python') - code.append("s = 'Python syntax highlighting") + code.append("s = 'Python syntax highlighting'") code.append("print(s)") writer.write(code) table = mg.Table() table.add_column('col1') - table.add_column('col2', mg.alignment.CENTER) + table.add_column('col2', mg.Alignment.CENTER) table.add_column('col3', 2) for i in range(3): table.append(*['e{}f{}'.format(i, j) for j in range(3)]) diff --git a/markdown_generator/__init__.py b/markdown_generator/__init__.py index 4d8055a..27b9e5d 100644 --- a/markdown_generator/__init__.py +++ b/markdown_generator/__init__.py @@ -2,7 +2,8 @@ from .code import Code from .emphasis import (emphasis, strong, strikethrough) from .image import Image -from .link import link +from .mdlink import link from .list import (List, CheckList) from .table import Table from .writer import Writer +from .alignment import Alignment diff --git a/markdown_generator/alignment.py b/markdown_generator/alignment.py index 753490b..553d4fd 100644 --- a/markdown_generator/alignment.py +++ b/markdown_generator/alignment.py @@ -1,22 +1,7 @@ -class ColumnAlignment(object): - def __init__(self, value): - self.value = value +from enum import IntEnum - def has_flag(self, flag): - if isinstance(flag, ColumnAlignment): - return self.value & flag.value > 0 - elif isinstance(flag, int): - return self.value & flag > 0 - return False - def __eq__(self, other): - if isinstance(other, ColumnAlignment): - return self.value == other.value - elif isinstance(other, int): - return self.value == other - return False - - -LEFT = ColumnAlignment(1) -RIGHT = ColumnAlignment(2) -CENTER = ColumnAlignment(3) +class Alignment(IntEnum): + LEFT = 1 + RIGHT = 2 + CENTER = 3 diff --git a/markdown_generator/blockquote.py b/markdown_generator/blockquote.py index 6c34532..c18ef3b 100644 --- a/markdown_generator/blockquote.py +++ b/markdown_generator/blockquote.py @@ -7,6 +7,8 @@ def append(self, text): self.lines.append(text) def __str__(self): + if not self.lines: + return '' lines = ['{} {}\n'.format(''.ljust(self.level, '>'), line) for line in self.lines] return ''.join(lines) + '\n' diff --git a/markdown_generator/code.py b/markdown_generator/code.py index 95aa6e9..1616a3b 100644 --- a/markdown_generator/code.py +++ b/markdown_generator/code.py @@ -7,9 +7,12 @@ def append(self, text): self.lines.append(text) def __str__(self): - lang = '' if self.language is None else self.language - lines = [] - lines.append('```{}'.format(lang)) - lines.extend(self.lines) - lines.append('```') - return '\n'.join(lines) + '\n' + if self.lines: + lang = '' if self.language is None else self.language + lines = [] + lines.append('```{}'.format(lang)) + lines.extend(self.lines) + lines.append('```') + return '\n'.join(lines) + '\n' + else: + return '' diff --git a/markdown_generator/emphasis.py b/markdown_generator/emphasis.py index d1df3a1..a3dd56a 100644 --- a/markdown_generator/emphasis.py +++ b/markdown_generator/emphasis.py @@ -1,5 +1,5 @@ def wrap(text, tag): - return tag + str(text) + tag + return tag + str(text) + tag if text else '' def emphasis(text): diff --git a/markdown_generator/image.py b/markdown_generator/image.py index 9c66637..4129413 100644 --- a/markdown_generator/image.py +++ b/markdown_generator/image.py @@ -4,6 +4,8 @@ def __init__(self, img_url, alt_text=None): self.alt_text = alt_text def __str__(self): + if not self.img_url: + return '' return '![{}]({})'.format(('' if self.alt_text is None else self.alt_text), self.img_url) diff --git a/markdown_generator/link.py b/markdown_generator/link.py deleted file mode 100644 index 37dd236..0000000 --- a/markdown_generator/link.py +++ /dev/null @@ -1,2 +0,0 @@ -def link(text, url): - return '[{}]({})'.format(text, url) diff --git a/markdown_generator/list.py b/markdown_generator/list.py index 016ce43..993c959 100644 --- a/markdown_generator/list.py +++ b/markdown_generator/list.py @@ -7,9 +7,16 @@ def append(self, item): self.items.append(item) def __str__(self): - marker = '1.' if self.ordered else '*' - result = ''.join('{} {}\n'.format(marker, item) for item in self.items) - return result + '\n' + if self.items: + def prepend(item, marker): + return '{} {}\n'.format(marker, item) + if self.ordered: + result = ''.join(prepend(x, '{}.'.format(i + 1)) + for i, x in enumerate(self.items)) + else: + result = ''.join(prepend(x, '*') for x in self.items) + return result + '\n' + return '' class CheckList(object): @@ -20,8 +27,10 @@ def append(self, item, done=False): self.items.append((item, done)) def __str__(self): - lines = [] - for item, done in self.items: - mark = 'x' if done else ' ' - lines.append('- [{}] {}\n'.format(mark, item)) - return ''.join(lines) + '\n' + if self.items: + lines = [] + for item, done in self.items: + mark = 'x' if done else ' ' + lines.append('- [{}] {}\n'.format(mark, item)) + return ''.join(lines) + '\n' + return '' diff --git a/markdown_generator/mdlink.py b/markdown_generator/mdlink.py new file mode 100644 index 0000000..be10334 --- /dev/null +++ b/markdown_generator/mdlink.py @@ -0,0 +1,6 @@ +def link(url, text=None): + if url: + if text: + return '[{}]({})'.format(text, url) + return url + return '' diff --git a/markdown_generator/table.py b/markdown_generator/table.py index f0c096e..73dce32 100644 --- a/markdown_generator/table.py +++ b/markdown_generator/table.py @@ -1,4 +1,4 @@ -from .alignment import LEFT, RIGHT, CENTER, ColumnAlignment +from .alignment import Alignment class Table(object): @@ -6,11 +6,9 @@ def __init__(self): self.columns = [] self.entries = [] - def add_column(self, heading, alignment=LEFT): + def add_column(self, heading, alignment=Alignment.LEFT): if isinstance(alignment, int): - alignment = ColumnAlignment(alignment) - if alignment not in [LEFT, CENTER, RIGHT]: - raise ValueError('Table.addColumn(): invalid alignment given') + alignment = Alignment(alignment) self.columns.append(Column(heading, alignment)) def append(self, *fields): @@ -23,20 +21,24 @@ def format_row(self, fields): return ' | '.join([''] + [str(f) for f in fields] + ['']).strip() def __str__(self): - titleRow = '|' - separatorRow = '|' - for column in self.columns: - titleRow += ' {} |'.format(column.heading) - separatorRow += '{}---{}|'.format( - ':' if column.alignment.has_flag(LEFT) else ' ', - ':' if column.alignment.has_flag(RIGHT) else ' ' - ) - result = '{}\n{}\n'.format(titleRow, separatorRow) - result += '\n'.join(map(self.format_row, self.entries)) - return result + '\n' + if self.columns: + titleRow = '|' + separatorRow = '|' + for column in self.columns: + titleRow += ' {} |'.format(column.heading) + separatorRow += '{}---{}|'.format( + ':' if bool(Alignment.LEFT & column.alignment) else ' ', + ':' if bool(Alignment.RIGHT & column.alignment) else ' ' + ) + result = '{}\n{}\n'.format(titleRow, separatorRow) + result += '\n'.join(map(self.format_row, self.entries)) + if self.entries: + result += '\n' + return result + '\n' + return '' class Column(object): - def __init__(self, heading, alignment=LEFT): + def __init__(self, heading, alignment=Alignment.LEFT): self.heading = heading self.alignment = alignment diff --git a/requirements.txt b/requirements.txt index 3c73d0f..4f21fd3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ flake8==3.5.0 pep8>1.7,<1.7.99 pyflakes==1.6.0 -nose==1.3.7 +ddt==1.1.1 +coveralls>=1.2.0 diff --git a/tests/helper.py b/tests/helper.py new file mode 100644 index 0000000..a861adb --- /dev/null +++ b/tests/helper.py @@ -0,0 +1,6 @@ +import unittest + + +class MarkdownTestCase(unittest.TestCase): + def assertRenderedEqual(self, obj, expected): + return self.assertEqual(str(obj), expected) diff --git a/tests/test_alignment.py b/tests/test_alignment.py new file mode 100644 index 0000000..63d760d --- /dev/null +++ b/tests/test_alignment.py @@ -0,0 +1,19 @@ +import unittest + +from markdown_generator import Alignment + + +class AlignmentTest(unittest.TestCase): + def test_alignment_none(self): + with self.assertRaises(ValueError): + Alignment(None) + + def test_alignment_from_value(self): + self.assertEqual(Alignment(1), 1) + + def test_invalid_alignment(self): + with self.assertRaises(ValueError): + Alignment(999) + + def test_left_or_right_is_center(self): + self.assertEqual(Alignment.LEFT | Alignment.RIGHT, Alignment.CENTER) diff --git a/tests/test_blockquote.py b/tests/test_blockquote.py new file mode 100644 index 0000000..f463c31 --- /dev/null +++ b/tests/test_blockquote.py @@ -0,0 +1,44 @@ +from .helper import MarkdownTestCase + +from markdown_generator import BlockQuote + + +class TestBlockQuote(MarkdownTestCase): + def assertRenderedEqual(self, bq, expected): + self.assertEqual(str(bq), expected) + + def test_empty(self): + self.assertRenderedEqual(BlockQuote(), '') + + def test_single_line(self): + bq = BlockQuote() + bq.append('My quote here') + self.assertRenderedEqual(bq, '> My quote here\n\n') + + def test_multiple_lines(self): + bq = BlockQuote() + bq.append('My quote here') + bq.append('Longer quote') + bq.append('Even longer') + expected = ('> My quote here\n' + '> Longer quote\n' + '> Even longer\n\n') + self.assertRenderedEqual(bq, expected) + + def test_level_other_than_one_empty(self): + self.assertRenderedEqual(BlockQuote(2), '') + + def test_level_other_than_one_single_line(self): + bq = BlockQuote(3) + bq.append('My quote here') + self.assertRenderedEqual(bq, '>>> My quote here\n\n') + + def test_level_other_than_one_multi_line(self): + bq = BlockQuote(2) + bq.append('My quote here') + bq.append('Longer quote') + bq.append('Even longer') + expected = ('>> My quote here\n' + '>> Longer quote\n' + '>> Even longer\n\n') + self.assertRenderedEqual(bq, expected) diff --git a/tests/test_code.py b/tests/test_code.py new file mode 100644 index 0000000..6cd18d8 --- /dev/null +++ b/tests/test_code.py @@ -0,0 +1,40 @@ +from .helper import MarkdownTestCase + +from markdown_generator import Code + + +class CodeTest(MarkdownTestCase): + def test_empty(self): + self.assertRenderedEqual(Code(), '') + + def test_empty_with_language(self): + code = Code('Python') + self.assertRenderedEqual(code, '') + + def test_single_line(self): + code = Code() + code.append('print("test")') + self.assertRenderedEqual(code, '```\nprint("test")\n```\n') + + def test_single_line_with_language(self): + code = Code('Python') + code.append('print("test")') + self.assertRenderedEqual(code, '```Python\nprint("test")\n```\n') + + def test_multiple_lines(self): + code = Code() + code.append('print("test")') + code.append('print("test2")') + self.assertRenderedEqual( + code, + '```\nprint("test")\nprint("test2")\n```\n' + ) + + def test_multiple_lines_with_language(self): + code = Code('Python') + code.append('print("test")') + code.append('print("test2")') + self.assertRenderedEqual( + code, + '```Python\nprint("test")\nprint("test2")\n```\n' + ) diff --git a/tests/test_emphasis.py b/tests/test_emphasis.py new file mode 100644 index 0000000..4309a28 --- /dev/null +++ b/tests/test_emphasis.py @@ -0,0 +1,37 @@ +from .helper import MarkdownTestCase + +from markdown_generator.emphasis import ( + emphasis, + strikethrough, + strong, + wrap, +) + + +class EmphasisTest(MarkdownTestCase): + def test_wrap(self): + self.assertEqual(wrap('test', 'tag'), 'tagtesttag') + + def test_wrap_empty_tag(self): + self.assertEqual(wrap('test', ''), 'test') + + def test_wrap_empty_text(self): + self.assertEqual(wrap('', 'tag'), '') + + def test_emphasis(self): + self.assertEqual(emphasis('text'), '*text*') + + def test_emphasis_empty(self): + self.assertEqual(emphasis(''), '') + + def test_strikethrough(self): + self.assertEqual(strikethrough('text'), '~~text~~') + + def test_strikethrough_empty(self): + self.assertEqual(strikethrough(''), '') + + def test_strong(self): + self.assertEqual(strong('text'), '**text**') + + def test_strong_empty(self): + self.assertEqual(strong(''), '') diff --git a/tests/test_image.py b/tests/test_image.py new file mode 100644 index 0000000..6173d0c --- /dev/null +++ b/tests/test_image.py @@ -0,0 +1,17 @@ +from .helper import MarkdownTestCase +from markdown_generator import Image + + +class ImageTest(MarkdownTestCase): + def test_url_empty(self): + self.assertRenderedEqual(Image(''), '') + + def test_url_none(self): + self.assertRenderedEqual(Image(''), '') + + def test_valid_url(self): + self.assertRenderedEqual(Image('image.png'), '![](image.png)') + + def test_valid_url_with_alt_text(self): + self.assertRenderedEqual(Image('image.png', 'image of thing'), + '![image of thing](image.png)') diff --git a/tests/test_link.py b/tests/test_link.py new file mode 100644 index 0000000..4ff9010 --- /dev/null +++ b/tests/test_link.py @@ -0,0 +1,27 @@ +from .helper import MarkdownTestCase +from markdown_generator import link + + +class LinkTest(MarkdownTestCase): + def test_empty_link(self): + self.assertEqual(link(''), '') + + def test_none_link(self): + self.assertEqual(link(None), '') + + def test_valid_link(self): + url = 'example.com' + self.assertEqual(link(url), url) + + def test_valid_link_none_text(self): + url = 'example.com' + self.assertEqual(link(url, None), url) + + def test_valid_link_empty_text(self): + url = 'example.com' + self.assertEqual(link(url, ''), url) + + def test_valid_link_with_text(self): + url = 'example.com' + text = 'see example' + self.assertEqual(link(url, text), '[{}]({})'.format(text, url)) diff --git a/tests/test_list.py b/tests/test_list.py new file mode 100644 index 0000000..a3d7d6d --- /dev/null +++ b/tests/test_list.py @@ -0,0 +1,79 @@ +from .helper import MarkdownTestCase +from markdown_generator import List, CheckList + + +class OrderedList(List): + def __init__(self): + List.__init__(self, True) + + +class UnorderedListTest(MarkdownTestCase): + def test_empty_list(self): + self.assertRenderedEqual(List(), '') + + def test_singleton_list(self): + lst = List() + lst.append('item') + self.assertRenderedEqual(lst, '* item\n\n') + + def test_list_with_multiple_items(self): + lst = List() + items = ['item{}'.format(i) for i in range(3)] + [lst.append(x) for x in items] + expected = '* ' + '\n* '.join(items) + '\n\n' + self.assertRenderedEqual(lst, expected) + + +class OrderedListTest(MarkdownTestCase): + def test_empty_list(self): + self.assertRenderedEqual(OrderedList(), '') + + def test_singleton_list(self): + lst = OrderedList() + lst.append('item') + self.assertRenderedEqual(lst, '1. item\n\n') + + def test_list_with_multiple_items(self): + lst = OrderedList() + items = ['item{}'.format(i) for i in range(3)] + [lst.append(x) for x in items] + items = ['{}. {}'.format(i + 1, x) for i, x in enumerate(items)] + expected = '\n'.join(items) + '\n\n' + self.assertRenderedEqual(lst, expected) + + +class CheckListTest(MarkdownTestCase): + def test_empty_list(self): + self.assertRenderedEqual(CheckList(), '') + + def test_singleton_list(self): + lst = CheckList() + lst.append('item') + self.assertRenderedEqual(lst, '- [ ] item\n\n') + + def test_singleton_list_checked(self): + lst = CheckList() + lst.append('item', True) + self.assertRenderedEqual(lst, '- [x] item\n\n') + + def test_multiple_items(self): + lst = CheckList() + items = ['item{}'.format(i) for i in range(3)] + [lst.append(x) for x in items] + expected = ''.join('- [ ] {}\n'.format(x) for x in items) + '\n' + self.assertRenderedEqual(lst, expected) + + def test_multiple_items_checked(self): + lst = CheckList() + items = ['item{}'.format(i) for i in range(3)] + [lst.append(x, True) for x in items] + expected = ''.join('- [x] {}\n'.format(x) for x in items) + '\n' + self.assertRenderedEqual(lst, expected) + + def test_multiple_items_mixed(self): + lst = CheckList() + items = [('item{}'.format(i), i % 2 == 0) for i in range(3)] + [lst.append(*x) for x in items] + items = ((x, 'x' if y else ' ') for x, y in items) + expected = ''.join('- [{}] {}\n'.format(x, y) for y, x in items) + '\n' + self.assertRenderedEqual(lst, expected) diff --git a/tests/test_table.py b/tests/test_table.py new file mode 100644 index 0000000..879c68b --- /dev/null +++ b/tests/test_table.py @@ -0,0 +1,79 @@ +from .helper import MarkdownTestCase +from markdown_generator import Table, Alignment +from ddt import ddt, data, unpack +from itertools import combinations, permutations + + +class Data(dict): + def __init__(self, name, **elements): + dict.__init__(self, **elements) + setattr(self, '__name__', name) + + +def create_test_data(scols='', rows=0): + cols = [{ + 'L': Alignment.LEFT, + 'R': Alignment.RIGHT, + 'C': Alignment.CENTER + }[c] for c in scols] + output = '' + if cols: + output += '| ' + output += ' | '.join('c{}'.format(i) for i, _ in enumerate(cols)) + output += ' |\n' + for col in cols: + output += '|' + output += ':' if Alignment.LEFT & col > 0 else ' ' + output += '---' + output += ':' if Alignment.RIGHT & col > 0 else ' ' + output += '|\n' + for r in range(rows): + for c, _ in enumerate(cols): + output += '| {},{} '.format(r, c) + output += '|\n' + output += '\n' + return Data( + '{}{}'.format(scols, rows) if scols else 'Empty', + columns=cols, + rows=rows, + expected=output + ) + + +alignments = { + ''.join(p) + for r in range(4) + for c in combinations('LRC', r) + for p in permutations(c) +} +TEST_DATA = [ + create_test_data(cols, i) + for cols in alignments + for i in range(3) +] + + +@ddt +class TableTest(MarkdownTestCase): + @data(*TEST_DATA) + @unpack + def test_table_format(self, columns=None, rows=0, expected=''): + if columns is None: + columns = [] + table = Table() + for i, col_align in enumerate(columns): + table.add_column('c{}'.format(i), col_align) + for r in range(rows): + table.append(*('{},{}'.format(r, c) for c in range(len(columns)))) + self.assertRenderedEqual(table, expected) + + def test_error_append_no_columns(self): + table = Table() + with self.assertRaises(ValueError): + table.append('0,0') + + def test_error_append_not_enough_columns(self): + table = Table() + table.add_column('c0') + with self.assertRaises(ValueError): + table.append('0,0', '0,1') diff --git a/tests/test_writer.py b/tests/test_writer.py new file mode 100644 index 0000000..83d8516 --- /dev/null +++ b/tests/test_writer.py @@ -0,0 +1,82 @@ +import unittest +from markdown_generator import Writer +from ddt import ddt, data, unpack +from os import linesep + + +class Data(dict): + def __init__(self, item=None, **elements): + dict.__init__(self, item=item, **elements) + setattr(self, '__name__', type(item).__name__) + + +class HeadingData(Data): + def __init__(self, item=None, level=1, **elements): + Data.__init__(self, level=level, item=item, **elements) + setattr(self, '__name__', getattr(self, '__name__') + str(level)) + + +class MockFile(object): + def __init__(self): + self.content = '' + + def clear(self): + self.content = '' + + def write(self, text): + self.content += text + + def read(self): + content = self.content + self.clear() + return content + + +WRITE_TEST_DATA = [Data(item) for item in [ + 'text', +]] +HEADER_TEST_DATA = [ + HeadingData(level=n, **d) + for n in range(1, 7) + for d in WRITE_TEST_DATA +] + + +@ddt +class WriterTest(unittest.TestCase): + def setUp(self): + self.stdout = MockFile() + self.writer = Writer(self.stdout) + + def assertReadEqual(self, text, msg=None): + self.assertEqual(self.stdout.read(), text, msg=msg) + + def test_no_writes(self): + self.assertReadEqual('') + + @data(*WRITE_TEST_DATA) + @unpack + def test_write(self, item): + self.writer.write(item) + self.assertReadEqual(str(item)) + + @data(*WRITE_TEST_DATA) + @unpack + def test_writeline(self, item): + self.writer.writeline(item) + self.assertReadEqual(str(item) + linesep) + + def test_writelines(self): + self.writer.writelines(WRITE_TEST_DATA) + expected = linesep.join(map(str, WRITE_TEST_DATA)) + linesep + self.assertReadEqual(expected) + + @data(*HEADER_TEST_DATA) + @unpack + def test_write_heading(self, item, level): + self.writer.write_heading(item, level) + self.assertReadEqual(''.ljust(level, '#') + ' ' + str(item) + linesep) + + def test_write_hrule(self): + self.writer.write_hrule() + self.assertReadEqual('---' + linesep) diff --git a/tests/tests.py b/tests/tests.py deleted file mode 100644 index d8174d2..0000000 --- a/tests/tests.py +++ /dev/null @@ -1,6 +0,0 @@ -import unittest - - -class TestPlaceholder(unittest.TestCase): - def test_placeholder(self): - pass