├── .gitignore ├── taskpaper ├── exceptions.py ├── __init__.py └── item.py ├── .travis.yml ├── test ├── utils.py └── test_item.py ├── setup.py ├── tox.ini └── .coveragerc /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .hypothesis 3 | *.egg-info 4 | .tox 5 | *.pyc 6 | __pycache__ 7 | .cache 8 | -------------------------------------------------------------------------------- /taskpaper/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | 4 | class TaskPaperError(Exception): 5 | pass 6 | -------------------------------------------------------------------------------- /taskpaper/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from .item import TaskPaperItem # noqa 4 | from .exceptions import TaskPaperError # noqa 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | - "3.3" 6 | - "3.4" 7 | - "3.5" 8 | - "pypy" 9 | 10 | sudo: false 11 | install: 12 | - "pip install tox" 13 | 14 | script: "tox" 15 | 16 | cache: 17 | directories: 18 | - .hypothesis 19 | -------------------------------------------------------------------------------- /test/utils.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from hypothesis.strategies import builds, text 4 | 5 | from taskpaper import TaskPaperItem 6 | 7 | 8 | def taskpaper_item_strategy(): 9 | return builds( 10 | TaskPaperItem, 11 | text=text() 12 | ) 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | 4 | from setuptools import setup 5 | 6 | 7 | setup( 8 | name="python-taskpaper", 9 | description="A Python module for working with TaskPaper-formatted documents", 10 | author="Alex Chan", 11 | author_email="alex@alexwlchan.net", 12 | packages=["taskpaper"], 13 | ) 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{27,33,34,35}, 4 | pypy, 5 | lint 6 | 7 | [testenv] 8 | commands = py.test -n 4 --cov taskpaper test 9 | deps = 10 | coverage 11 | hypothesis 12 | pytest 13 | pytest-cov 14 | pytest-xdist 15 | 16 | [testenv:lint] 17 | basepython = python3.5 18 | commands = flake8 --max-complexity 10 taskpaper test 19 | deps = 20 | flake8 21 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = taskpaper 4 | 5 | [report] 6 | fail_under = 100 7 | show_missing = True 8 | exclude_lines = 9 | pragma: no cover 10 | .*:.* # Python \d.* 11 | assert False, "Should not be reachable" 12 | .*:.* # Platform-specific: 13 | 14 | [paths] 15 | source = 16 | taskpaper/ 17 | .tox/*/lib/python*/site-packages/taskpaper 18 | .tox/pypy*/site-packages/taskpaper -------------------------------------------------------------------------------- /taskpaper/item.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from .exceptions import TaskPaperError 4 | 5 | 6 | class TaskPaperItem(object): 7 | 8 | def __init__(self, text, tab_size=4, parent=None): 9 | """ 10 | :param tab_size: Number of spaces used per indentation level. 11 | :param parent: Parent task. 12 | """ 13 | self.text = text 14 | self.tab_size = tab_size 15 | self.children = [] 16 | self.parent = parent 17 | 18 | def __repr__(self): 19 | return '%s(text=%r, tab_size=%r, parent=%r)' % ( 20 | type(self).__name__, 21 | self.text, 22 | self.tab_size, 23 | self.parent, 24 | ) 25 | 26 | @property 27 | def parent(self): 28 | return self._parent 29 | 30 | @parent.setter 31 | def parent(self, new_parent): 32 | # If we've never had a parent, default to None before proceeding 33 | if not hasattr(self, '_parent'): 34 | self._parent = None 35 | 36 | # If this is the same as our old parent, this is a no-op 37 | if self._parent is new_parent: 38 | return 39 | 40 | # Check that we aren't about to make this item its own parent 41 | if new_parent is self: 42 | raise TaskPaperError( 43 | 'Cannot make %r a parent of itself' 44 | ) 45 | 46 | # Check that we aren't about to form a circular cycle of parents -- 47 | # in particular, that we aren't our new parent's parent, or any of 48 | # its ancestors. 49 | if new_parent is not None: 50 | for ancestor in new_parent.ancestors: 51 | if ancestor is self: 52 | raise TaskPaperError( 53 | 'Making %r a parent of %r would create a circular ' 54 | 'family tree' % (self, new_parent) 55 | ) 56 | 57 | # If we already had a parent, remove ourselves from its children 58 | if self._parent is not None: 59 | self._parent.children.remove(self) 60 | 61 | # If the new parent is not None, add ourselves to its children 62 | if new_parent is not None: 63 | new_parent.children.append(self) 64 | 65 | self._parent = new_parent 66 | 67 | @property 68 | def ancestors(self): 69 | current = self 70 | while current.parent is not None: 71 | yield current.parent 72 | current = current.parent 73 | -------------------------------------------------------------------------------- /test/test_item.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from hypothesis import given 4 | from hypothesis.strategies import integers, lists 5 | import pytest 6 | 7 | from taskpaper import TaskPaperItem, TaskPaperError 8 | 9 | from utils import taskpaper_item_strategy 10 | 11 | 12 | @given(integers()) 13 | def test_setting_tab_size(tab_size): 14 | """We can set the tab size on TaskPaperItem.""" 15 | item = TaskPaperItem('hello world', tab_size=tab_size) 16 | assert item.tab_size == tab_size 17 | 18 | 19 | class TestParentChildRelationship(object): 20 | """ 21 | Tests of the parent-child relationship between items. 22 | """ 23 | 24 | def test_default_parent_is_none(self): 25 | """By default, a task does not have a parent.""" 26 | item = TaskPaperItem('hello world') 27 | assert item.parent is None 28 | 29 | def test_default_task_has_no_children(self): 30 | """By default, a task has no children.""" 31 | item = TaskPaperItem('hello world') 32 | assert item.children == [] 33 | 34 | def test_setting_a_parent(self): 35 | """Test we can initialize an item with a parent.""" 36 | item_p = TaskPaperItem('parent') 37 | item_c = TaskPaperItem('child', parent=item_p) 38 | assert item_c.parent == item_p 39 | assert item_p.children == [item_c] 40 | 41 | def test_updating_a_parent(self): 42 | """Test we can create an item with a parent, then change the parent.""" 43 | item_p1 = TaskPaperItem('parent1') 44 | item_p2 = TaskPaperItem('parent2') 45 | item_c = TaskPaperItem('child', parent=item_p1) 46 | 47 | item_c.parent = item_p2 48 | assert item_c.parent == item_p2 49 | assert item_p2.children == [item_c] 50 | assert item_p1.children == [] 51 | 52 | def test_updating_to_same_parent(self): 53 | """ 54 | Create an item with a parent, change the parent to existing parent, 55 | check nothing happens. 56 | """ 57 | item_p = TaskPaperItem('parent') 58 | item_c = TaskPaperItem('child', parent=item_p) 59 | 60 | item_c.parent == item_p 61 | assert item_c.parent == item_p 62 | assert item_p.children == [item_c] 63 | 64 | def test_removing_a_parent(self): 65 | """ 66 | Create an item with a parent, then set the parent to None. Check the 67 | child is removed from the list of its previous parents' children. 68 | """ 69 | item_p = TaskPaperItem('parent') 70 | item_c = TaskPaperItem('child', parent=item_p) 71 | 72 | item_c.parent = None 73 | assert item_c.parent is None 74 | assert item_p.children == [] 75 | 76 | def test_detect_item_cannot_be_its_parents_parent(self): 77 | """ 78 | An item cannot be the parent of its own parent. 79 | """ 80 | item_p = TaskPaperItem('parent') 81 | item_c = TaskPaperItem('child', parent=item_p) 82 | 83 | with pytest.raises(TaskPaperError): 84 | item_p.parent = item_c 85 | 86 | @given(lists(taskpaper_item_strategy(), min_size=2)) 87 | def test_detecting_circular_chain(self, items): 88 | """ 89 | We detect an arbitrarily long circular parent chain. 90 | """ 91 | # Create a chain of parent-child relationships 92 | # items[0] -> items[1] -> ... -> items[n] 93 | for idx, alt_item in enumerate(items[1:], start=1): 94 | items[idx-1].parent = alt_item 95 | 96 | # Now make the first item the parent of the last, and check we 97 | # get an exception. 98 | with pytest.raises(TaskPaperError): 99 | items[-1].parent = items[0] 100 | 101 | def test_an_item_cannot_be_its_own_parent(self): 102 | """ 103 | An item cannot be its own parent. 104 | """ 105 | item = TaskPaperItem('hello world') 106 | with pytest.raises(TaskPaperError): 107 | item.parent = item 108 | --------------------------------------------------------------------------------