Commit 8b1ff41a authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge branch 'master' into column-ui/master

Conflicts:
	README.md
	test/test_list_format.py
	topydo/cli/CLIApplicationBase.py
	topydo/lib/Config.py
	topydo/lib/Utils.py
parents d2ba87c3 4b8c8f09
......@@ -4,9 +4,9 @@
* Dropped support for Python 2.7.
* Add ability to filter on creation/completion dates:
topydo ls created:today
topydo ls completed:today
topydo -t done.txt completed:today # if auto-archiving is set
topydo ls created:today
topydo ls completed:today
topydo -t done.txt completed:today # if auto-archiving is set
* `ls -F` supports `%P` that expands to a single space when no priority is set,
in contrast to `%p` which expands to an empty string (thanks to @MinchinWeb).
......@@ -41,8 +41,8 @@ for the majority of these new features.
* `ls` output can be customized with a -F flag or a configuration option:
[ls]
list_format = |%I| %x %{(}p{)} %c %s %k %{due:}d %{t:}t
[ls]
list_format = |%I| %x %{(}p{)} %c %s %k %{due:}d %{t:}t
or `ls -F "%{(}p{)} %s %{due:}d"`.
......
......@@ -43,10 +43,6 @@ smoothly into topydo.
exit 1
fi
if ! python2 -m pylint --errors-only topydo test; then
exit 1
fi
if ! python3 -m pylint --errors-only topydo test; then
exit 1
fi
......
......@@ -26,22 +26,24 @@ Simply install with:
pip install topydo
### Optional dependencies
### Dependencies
* [arrow][8] : Used to turn dates into a human readable version.
#### Optional dependencies:
* [icalendar][7] : To print your todo.txt file as an iCalendar file
(not supported for Python 3.2).
(not supported for PyPy3).
* [prompt-toolkit][6] : For topydo's _prompt_ mode, which offers a shell-like
interface with auto-completion.
* [arrow][8] : Used to turn dates into a human readable version.
* [urwid][12] : For topydo's _columns_ mode, a TUI with columns for
your todo items.
* [arrow][8] : Used to turn dates into a human readable version.
* [backports.shutil_get_terminal_size][9] : Used to determine your terminal
window size. This function was
added to the standard library in
Python 3.3 and so is only
required in older versions of
Python.
* [python-dateutil][10]: A dependency of *arrow*.
required for PyPy3.
* [mock][11] : Used for testing. This was added to the standard
library in Python 3.3.
......
......@@ -20,7 +20,7 @@ from topydo.lib.Utils import escape_ansi
class CommandTest(TopydoTest):
def __init__(self, *args, **kwargs):
super(CommandTest, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.output = ""
self.errors = ""
......
......@@ -4,3 +4,4 @@ baz = FooBar
format = ls -F "|I| x c d {(}p{)} s k" -n 25
smile = ls
star = tag {} star 1
quot = lol'd
......@@ -33,7 +33,7 @@ except ImportError:
class AddCommandTest(CommandTest):
def setUp(self):
super(AddCommandTest, self).setUp()
super().setUp()
self.todolist = TodoList.TodoList([])
self.today = date.today().isoformat()
......
......@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList
class AppendCommandTest(CommandTest):
def setUp(self):
super(AppendCommandTest, self).setUp()
super().setUp()
self.todolist = TodoList([])
self.todolist.add("Foo")
......
......@@ -33,7 +33,7 @@ def _no_prompt(self):
class DeleteCommandTest(CommandTest):
def setUp(self):
super(DeleteCommandTest, self).setUp()
super().setUp()
todos = [
"Foo id:1",
"Bar p:1",
......
......@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList
class DepCommandTest(CommandTest):
def setUp(self):
super(DepCommandTest, self).setUp()
super().setUp()
todos = [
"Foo id:1",
"Bar p:1",
......
......@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList
class DepriCommandTest(CommandTest):
def setUp(self):
super(DepriCommandTest, self).setUp()
super().setUp()
todos = [
"(A) Foo",
"Bar",
......
......@@ -32,7 +32,7 @@ def _no_prompt(self):
class DoCommandTest(CommandTest):
def setUp(self):
super(DoCommandTest, self).setUp()
super().setUp()
todos = [
"Foo id:1",
"Bar p:1",
......
......@@ -33,7 +33,7 @@ except ImportError:
class EditCommandTest(CommandTest):
def setUp(self):
super(EditCommandTest, self).setUp()
super().setUp()
todos = [
"Foo id:1",
"Bar p:1 @test",
......
......@@ -301,7 +301,7 @@ class FilterTest(TopydoTest):
class OrdinalTagFilterTest(TopydoTest):
def setUp(self):
super(OrdinalTagFilterTest, self).setUp()
super().setUp()
today = date.today()
tomorrow = today + timedelta(1)
......@@ -381,7 +381,7 @@ class OrdinalTagFilterTest(TopydoTest):
class CreationFilterTest(TopydoTest):
def setUp(self):
super(CreationFilterTest, self).setUp()
super().setUp()
self.todo1 = "2015-12-19 With creation date."
self.todo2 = "Without creation date."
......@@ -415,7 +415,7 @@ class CreationFilterTest(TopydoTest):
class CompletionFilterTest(TopydoTest):
def setUp(self):
super(CompletionFilterTest, self).setUp()
super().setUp()
self.todo1 = "2015-12-19 With creation date."
self.todo2 = "x 2015-12-19 2015-12-18 Without creation date."
......@@ -459,7 +459,7 @@ class CompletionFilterTest(TopydoTest):
class PriorityFilterTest(TopydoTest):
def setUp(self):
super(PriorityFilterTest, self).setUp()
super().setUp()
self.todo1 = "(A) Foo"
self.todo2 = "(B) Bar"
......
......@@ -21,9 +21,8 @@ from topydo.Commands import get_subcommand
from topydo.commands.AddCommand import AddCommand
from topydo.commands.DeleteCommand import DeleteCommand
from topydo.commands.ListCommand import ListCommand
from topydo.commands.ListProjectCommand import ListProjectCommand
from topydo.commands.TagCommand import TagCommand
from topydo.lib.Config import config
from topydo.lib.Config import config, ConfigError
class GetSubcommandTest(TopydoTest):
def test_normal_cmd(self):
......@@ -120,6 +119,15 @@ class GetSubcommandTest(TopydoTest):
real_cmd, final_args = get_subcommand(args)
self.assertEqual(real_cmd, None)
def test_alias_quotation(self):
config("test/data/aliases.conf")
args = ["quot"]
with self.assertRaises(ConfigError) as ce:
get_subcommand(args)
self.assertEqual(str(ce.exception), 'No closing quotation')
def test_help(self):
real_cmd, final_args = get_subcommand(['help', 'nonexisting'])
self.assertFalse(real_cmd)
......
......@@ -22,7 +22,7 @@ from topydo.lib.Graph import DirectedGraph
class GraphTest(TopydoTest):
def setUp(self):
super(GraphTest, self).setUp()
super().setUp()
self.graph = DirectedGraph()
......
......@@ -35,7 +35,7 @@ except ImportError:
class ListCommandTest(CommandTest):
def setUp(self):
super(ListCommandTest, self).setUp()
super().setUp()
self.todolist = load_file_to_todolist("test/data/ListCommandTest.txt")
self.terminal_size = namedtuple('terminal_size', ['columns', 'lines'])
......@@ -397,7 +397,7 @@ class ListCommandTest(CommandTest):
class ListCommandUnicodeTest(CommandTest):
def setUp(self):
super(ListCommandUnicodeTest, self).setUp()
super().setUp()
self.todolist = load_file_to_todolist("test/data/ListCommandUnicodeTest.txt")
def test_list_unicode1(self):
......
......@@ -36,7 +36,7 @@ except ImportError:
@freeze_time("2015, 11, 06")
class ListFormatTest(CommandTest):
def setUp(self):
super(ListFormatTest, self).setUp()
super().setUp()
self.todolist = load_file_to_todolist("test/data/ListFormat.txt")
self.terminal_size = namedtuple('terminal_size', ['columns', 'lines'])
......@@ -132,7 +132,7 @@ class ListFormatTest(CommandTest):
command.execute()
result = """| 1| D Bar @Context1 +Project2 (3 months ago, due a month ago, started a month ago)
| 2| Z Lorem ipsum dolorem sit amet. Red @f... lazy:bar (just now, due in 2 days, starts in a day)
| 2| Z Lorem ipsum dolorem sit amet. Red @fox ... lazy:bar (today, due in 2 days, starts in a day)
| 3| C Foo @Context2 Not@Context +Project1 Not+Project (4 months ago)
| 4| C Baz @Context1 +Project1 key:value
| 5| Drink beer @ home
......@@ -183,7 +183,7 @@ x 2014-12-12
command.execute()
result = """3 months ago | a month ago | a month ago |
just now | in 2 days | in a day |
today | in 2 days | in a day |
4 months ago | | |
| | |
| | |
......@@ -278,7 +278,7 @@ just now | in 2 days | in a day |
command.execute()
result = """3 months ago
just now
today
4 months ago
......@@ -330,7 +330,7 @@ due in 2 days, starts in a day
command.execute()
result = """3 months ago, due a month ago, started a month ago
just now, due in 2 days, starts in a day
today, due in 2 days, starts in a day
4 months ago
......@@ -697,5 +697,19 @@ C -
"""
self.assertEqual(self.output, result)
def test_list_format46(self):
command = ListCommand(["-x", "-F", "%r"], self.todolist, self.out, self.error)
command.execute()
result = """(D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29
(Z) 2015-11-06 Lorem ipsum dolorem sit amet. Red @fox +jumped over the lazy:bar and jar due:2015-11-08 t:2015-11-07
(C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project
(C) Baz @Context1 +Project1 key:value
Drink beer @ home id:1 p:2 ical:foobar
x 2014-12-12 Completed but with date:2014-12-12
"""
self.assertEqual(self.output, result)
if __name__ == '__main__':
unittest.main()
......@@ -24,7 +24,7 @@ from topydo.lib.TodoList import TodoList
class PostponeCommandTest(CommandTest):
def setUp(self):
super(PostponeCommandTest, self).setUp()
super().setUp()
self.today = date.today()
self.past = date.today() - timedelta(1)
self.future = date.today() + timedelta(1)
......
......@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList
class PriorityCommandTest(CommandTest):
def setUp(self):
super(PriorityCommandTest, self).setUp()
super().setUp()
todos = [
"(A) Foo",
"Bar",
......
......@@ -25,7 +25,7 @@ from topydo.lib.Todo import Todo
class RecurrenceTest(TopydoTest):
def setUp(self):
super(RecurrenceTest, self).setUp()
super().setUp()
self.todo = Todo("Test rec:1w")
self.stricttodo = Todo("Test rec:+1w")
......
......@@ -15,7 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
from datetime import date, timedelta
from datetime import date
from freezegun import freeze_time
from test.topydo_testcase import TopydoTest
......@@ -25,7 +25,7 @@ from topydo.lib.RelativeDate import relative_date_to_date
@freeze_time('2015, 11, 06')
class RelativeDateTester(TopydoTest):
def setUp(self):
super(RelativeDateTester, self).setUp()
super().setUp()
self.yesterday = date(2015, 11, 5)
self.today = date(2015, 11, 6)
self.tomorrow = date(2015, 11, 7)
......@@ -40,6 +40,18 @@ class RelativeDateTester(TopydoTest):
result = relative_date_to_date('1d')
self.assertEqual(result, self.tomorrow)
def test_zero_bdays(self):
result = relative_date_to_date('0b')
self.assertEqual(result, self.today)
def test_one_bday(self):
result = relative_date_to_date('1b')
self.assertEqual(result, self.monday)
def test_one_bweek(self):
result = relative_date_to_date('5b')
self.assertEqual(result, self.friday)
def test_one_week(self):
result = relative_date_to_date('1w')
self.assertEqual(result, date(2015, 11, 13))
......@@ -152,6 +164,14 @@ class RelativeDateTester(TopydoTest):
result = relative_date_to_date('-0d')
self.assertTrue(result, self.today)
def test_negative_period3(self):
result = relative_date_to_date('-1b')
self.assertEqual(result, date(2015, 11, 5))
def test_negative_period4(self):
result = relative_date_to_date('-5b')
self.assertEqual(result, date(2015, 10, 30))
def test_weekday_next_week(self):
"""
When entering "Friday" on a Friday, return next week Friday instead of
......
......@@ -35,7 +35,7 @@ from topydo.lib.TodoList import TodoList
class RevertCommandTest(CommandTest):
def setUp(self):
super(RevertCommandTest, self).setUp()
super().setUp()
todos = [
"Foo",
"Bar",
......
......@@ -24,7 +24,7 @@ from topydo.lib.Config import config
class SortCommandTest(CommandTest):
def setUp(self):
super(SortCommandTest, self).setUp()
super().setUp()
self.todolist = load_file_to_todolist("test/data/SorterTest1.txt")
def test_sort1(self):
......
......@@ -24,7 +24,7 @@ from topydo.lib.TodoList import TodoList
class TagCommandTest(CommandTest):
def setUp(self):
super(TagCommandTest, self).setUp()
super().setUp()
todos = [
"Foo",
"Bar due:2014-10-22",
......
......@@ -29,7 +29,7 @@ from topydo.lib.TodoListBase import InvalidTodoException, TodoListBase
class TodoListTester(TopydoTest):
def setUp(self):
super(TodoListTester, self).setUp()
super().setUp()
self.todofile = TodoFile('test/data/TodoListTest.txt')
lines = [line for line in self.todofile.read()
......@@ -219,6 +219,13 @@ class TodoListTester(TopydoTest):
config("test/data/todolist-uid.conf")
self.assertRaises(InvalidTodoException, self.todolist.todo, 1)
def test_uid4(self):
"""
Handle UIDs properly when line numbers are configured.
"""
config(p_overrides={('topydo', 'identifiers'): 'linenumber'})
self.assertRaises(InvalidTodoException, self.todolist.todo, '11a')
def test_new_uid(self):
""" Make sure that item has new text ID after append. """
config("test/data/todolist-uid.conf")
......@@ -227,10 +234,23 @@ class TodoListTester(TopydoTest):
self.assertNotEqual(self.todolist.number(todo), 't5c')
def test_iteration(self):
""" Confirms that the iternation method is working. """
results = ["(C) Foo @Context2 Not@Context +Project1 Not+Project",
"(D) Bar @Context1 +Project2",
"(C) Baz @Context1 +Project1 key:value",
"(C) Drink beer @ home",
"(C) 13 + 29 = 42"]
i = 0
for todo in self.todolist:
self.assertEqual(todo.src, results[i])
i += 1
class TodoListDependencyTester(TopydoTest):
def setUp(self):
super(TodoListDependencyTester, self).setUp()
super().setUp()
self.todolist = TodoList([])
self.todolist.add("Foo id:1")
......@@ -242,6 +262,7 @@ class TodoListDependencyTester(TopydoTest):
self.todolist.add("Another one with +Project")
self.todolist.add("Todo with +AnotherProject")
self.todolist.add("Todo without children id:3")
self.todolist.add("Orphan p:4")
def test_check_dep(self):
children = self.todolist.children(self.todolist.todo(1))
......@@ -272,8 +293,8 @@ class TodoListDependencyTester(TopydoTest):
todo5 = self.todolist.todo(5)
self.todolist.add_dependency(todo5, todo4)
self.assertTrue(todo5.has_tag('id', '4'))
self.assertTrue(todo4.has_tag('p', '4'))
self.assertTrue(todo5.has_tag('id', '5'))
self.assertTrue(todo4.has_tag('p', '5'))
def test_add_dep2(self):
"""
......@@ -287,8 +308,8 @@ class TodoListDependencyTester(TopydoTest):
self.todolist.add_dependency(todo5, todo4)
self.todolist.add_dependency(todo4, todo1)
self.assertTrue(todo4.has_tag('id', '5'))
self.assertTrue(todo1.has_tag('p', '5'))
self.assertTrue(todo4.has_tag('id', '6'))
self.assertTrue(todo1.has_tag('p', '6'))
def test_add_dep3(self):
"""
......@@ -322,6 +343,7 @@ class TodoListDependencyTester(TopydoTest):
self.assertFalse(from_todo.has_tag('id'))
self.assertFalse(to_todo.has_tag('p'))
self.assertFalse(self.todolist.todo_by_dep_id('2'))
def test_remove_dep2(self):
old = str(self.todolist)
......@@ -330,6 +352,9 @@ class TodoListDependencyTester(TopydoTest):
self.todolist.remove_dependency(from_todo, to_todo)
self.assertEqual(str(self.todolist), old)
self.assertTrue(self.todolist.todo_by_dep_id('1'))
self.assertTrue(self.todolist.todo_by_dep_id('2'))
self.assertTrue(self.todolist.todo_by_dep_id('3'))
def test_remove_dep3(self):
""" Try to remove non-existing dependency. """
......@@ -339,6 +364,9 @@ class TodoListDependencyTester(TopydoTest):
self.todolist.remove_dependency(from_todo, to_todo)
self.assertEqual(str(self.todolist), old)
self.assertTrue(self.todolist.todo_by_dep_id('1'))
self.assertTrue(self.todolist.todo_by_dep_id('2'))
self.assertTrue(self.todolist.todo_by_dep_id('3'))
def test_remove_todo_check_children(self):
todo = self.todolist.todo(2)
......@@ -351,6 +379,7 @@ class TodoListDependencyTester(TopydoTest):
todo = self.todolist.todo(3)
self.todolist.delete(todo)
self.assertFalse(todo.has_tag('p', '2'))
self.assertFalse(self.todolist.todo_by_dep_id('2'))
todo = self.todolist.todo(1)
children = self.todolist.children(todo)
......@@ -372,6 +401,20 @@ class TodoListDependencyTester(TopydoTest):
self.assertTrue(todolist.todo_by_dep_id('1'))
self.assertFalse(todolist.todo_by_dep_id('2'))
def test_add_after_dependencies(self):
"""
Test that information is properly stored after dependency related
information was retrieved from the todo list.
"""
todo = self.todolist.todo(1)
self.todolist.parents(todo)
self.todolist.add('New dependency id:99')
self.todolist.add('Child p:99')
self.assertTrue(self.todolist.dirty)
self.assertTrue(self.todolist.todo_by_dep_id('99'))
class TodoListCleanDependencyTester(TopydoTest):
"""
......@@ -383,7 +426,7 @@ class TodoListCleanDependencyTester(TopydoTest):
"""
def setUp(self):
super(TodoListCleanDependencyTester, self).setUp()
super().setUp()
self.todolist = TodoList([])
def test_clean_dependencies1(self):
......@@ -419,19 +462,7 @@ class TodoListCleanDependencyTester(TopydoTest):
self.todolist.clean_dependencies()
self.assertFalse(self.todolist.todo(1).has_tag('id'))
def test_clean_dependencies4(self):
""" Clean p: items when siblings are still connected to parent. """
self.todolist.add("Foo id:1")
self.todolist.add("Bar p:1")
self.todolist.add("Baz p:1 id:2")
self.todolist.add("Buzz p:2 p:1")
self.todolist.clean_dependencies()
self.assertFalse(self.todolist.todo(4).has_tag('p', '1'))
self.assertTrue(self.todolist.todo(1).has_tag('id', '1'))
self.assertTrue(self.todolist.todo(2).has_tag('p', '1'))
self.assertFalse(self.todolist.todo_by_dep_id('1'))
if __name__ == '__main__':
......
......@@ -21,7 +21,7 @@ instance based on an argument list.
import sys
from topydo.lib.Config import config
from topydo.lib.Config import config, ConfigError
_SUBCOMMAND_MAP = {
'add': 'AddCommand',
......@@ -90,7 +90,11 @@ def get_subcommand(p_args):
If alias resolves to non-existent command, main help message is
returned.
"""
real_subcommand, alias_args = alias_map[p_alias]
try:
real_subcommand, alias_args = alias_map[p_alias]
except ValueError as ve:
raise ConfigError(alias_map[p_alias]) from ve
try:
result = import_subcommand(real_subcommand)
args = join_args(p_args, alias_args)
......
......@@ -41,7 +41,7 @@ class CLIApplication(CLIApplicationBase):
"""
def __init__(self):
super(CLIApplication, self).__init__()
super().__init__()
def run(self):
""" Main entry function. """
......@@ -50,7 +50,11 @@ class CLIApplication(CLIApplicationBase):
self.todofile = TodoFile.TodoFile(config().todotxt())
self.todolist = TodoList.TodoList(self.todofile.read())
(subcommand, args) = get_subcommand(args)
try:
(subcommand, args) = get_subcommand(args)
except ConfigError as ce:
error('Error: ' + str(ce) + '. Check your aliases configuration')
sys.exit(1)
if subcommand is None:
self._usage()
......
......@@ -226,9 +226,9 @@ class CLIApplicationBase(object):
to the todo.txt file.
"""
# do not archive when the value of the filename is an empty string
# (i.e. explicitly left empty in the configuration
if self.todolist.dirty:
# do not archive when the value of the filename is an empty string
# (i.e. explicitly left empty in the configuration
if self.do_archive and config().archive():
self._archive()
......
......@@ -59,7 +59,7 @@ class PromptApplication(CLIApplicationBase):
"""
def __init__(self):
super(PromptApplication, self).__init__()
super().__init__()
self._process_flags()
self.mtime = None
......@@ -95,11 +95,20 @@ class PromptApplication(CLIApplicationBase):
completer=self.completer,
complete_while_typing=False)
user_input = shlex.split(user_input)
except (EOFError, KeyboardInterrupt):
except EOFError:
sys.exit(0)
except KeyboardInterrupt:
continue
except ValueError as verr:
error('Error: ' + str(verr))
mtime_after = _todotxt_mtime()
(subcommand, args) = get_subcommand(user_input)
try:
(subcommand, args) = get_subcommand(user_input)
except ConfigError as ce:
error('Error: ' + str(ce) + '. Check your aliases configuration')
continue
# refuse to perform operations such as 'del' and 'do' if the
# todo.txt file has been changed in the background.
......
......@@ -34,7 +34,7 @@ class AddCommand(Command):
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(AddCommand, self).__init__(
super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
self.text = ' '.join(p_args)
self.from_file = None
......@@ -119,7 +119,7 @@ class AddCommand(Command):
def execute(self):
""" Adds a todo item to the list. """
if not super(AddCommand, self).execute():
if not super().execute():
return False
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
......
......@@ -24,11 +24,11 @@ class AppendCommand(Command):
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(AppendCommand, self).__init__(p_args, p_todolist, p_out, p_err,
super().__init__(p_args, p_todolist, p_out, p_err,
p_prompt)
def execute(self):
if not super(AppendCommand, self).execute():
if not super().execute():
return False
try:
......
......@@ -27,7 +27,7 @@ class ArchiveCommand(Command):
TodoListBase class which does no dependency checking, so a better
choice for huge done.txt files.
"""
super(ArchiveCommand, self).__init__([], p_todolist)
super().__init__([], p_todolist)
self.archive = p_archive_list
def execute(self):
......
......@@ -22,7 +22,7 @@ class DeleteCommand(DCommand):
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(DeleteCommand, self).__init__(
super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
def prompt_text(self):
......
......@@ -28,7 +28,7 @@ class DepCommand(Command):
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(DepCommand, self).__init__(
super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
try:
......@@ -109,7 +109,7 @@ class DepCommand(Command):
self.error(self.usage())
def execute(self):
if not super(DepCommand, self).execute():
if not super().execute():
return False
dispatch = {
......
......@@ -23,7 +23,7 @@ class DepriCommand(MultiCommand):
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(DepriCommand, self).__init__(
super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
def _execute_multi_specific(self):
......
......@@ -33,17 +33,17 @@ class DoCommand(DCommand):
self.strict_recurrence = False
self.completion_date = date.today()
super(DoCommand, self).__init__(
super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
def get_flags(self):
""" Additional flags. """
opts, long_opts = super(DoCommand, self).get_flags()
opts, long_opts = super().get_flags()
return ("d:s" + opts, ["date=", "strict"] + long_opts)
def process_flag(self, p_opt, p_value):
super(DoCommand, self).process_flag(p_opt, p_value)
super().process_flag(p_opt, p_value)
if p_opt == "-s" or p_opt == "--strict":
self.strict_recurrence = True
......
......@@ -39,7 +39,7 @@ def _is_edited(p_orig_mtime, p_file):
class EditCommand(MultiCommand):
def __init__(self, p_args, p_todolist, p_output, p_error, p_input):
super(EditCommand, self).__init__(p_args, p_todolist, p_output,
super().__init__(p_args, p_todolist, p_output,
p_error, p_input)
if len(self.args) == 0:
......
......@@ -26,11 +26,11 @@ class ExitCommand(Command):
"""
def __init__(self, p_args, p_todolist, p_output, p_error, p_input):
super(ExitCommand, self).__init__(p_args, p_todolist, p_output, p_error,
super().__init__(p_args, p_todolist, p_output, p_error,
p_input)
def execute(self):
if not super(ExitCommand, self).execute():
if not super().execute():
return False
sys.exit(0)
......@@ -28,7 +28,7 @@ class ListCommand(ExpressionCommand):
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(ListCommand, self).__init__(
super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
self.printer = None
......@@ -92,7 +92,7 @@ class ListCommand(ExpressionCommand):
Additional filters to select particular todo items given with the -i
flag.
"""
filters = super(ListCommand, self)._filters()
filters = super()._filters()
if self.ids:
def get_todo(p_id):
......@@ -122,7 +122,6 @@ class ListCommand(ExpressionCommand):
# create a standard printer with some filters
indent = config().list_indent()
final_format = ' ' * indent + self.format
hidden_tags = config().hidden_tags()
filters = []
filters.append(PrettyPrinterFormatFilter(self.todolist, final_format))
......@@ -132,7 +131,7 @@ class ListCommand(ExpressionCommand):
self.out(self.printer.print_list(self._view().todos))
def execute(self):
if not super(ListCommand, self).execute():
if not super().execute():
return False
try:
......
......@@ -22,11 +22,11 @@ class ListContextCommand(Command):
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(ListContextCommand, self).__init__(
super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
def execute(self):
if not super(ListContextCommand, self).execute():
if not super().execute():
return False
for context in sorted(self.todolist.contexts(), key=lambda s: s.lower()):
......
......@@ -22,11 +22,11 @@ class ListProjectCommand(Command):
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(ListProjectCommand, self).__init__(
super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
def execute(self):
if not super(ListProjectCommand, self).execute():
if not super().execute():
return False
for project in sorted(self.todolist.projects(), key=lambda s: s.lower()):
......
......@@ -28,7 +28,7 @@ class PostponeCommand(MultiCommand):
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(PostponeCommand, self).__init__(
super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
self.move_start_date = False
......
......@@ -26,7 +26,7 @@ class PriorityCommand(MultiCommand):
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(PriorityCommand, self).__init__(
super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
self.last_argument = True
......
......@@ -25,11 +25,11 @@ class RevertCommand(Command):
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(RevertCommand, self).__init__(p_args, p_todolist, p_out, p_err,
super().__init__(p_args, p_todolist, p_out, p_err,
p_prompt)
def execute(self):
if not super(RevertCommand, self).execute():
if not super().execute():
return False
archive_file = TodoFile.TodoFile(config().archive())
......
......@@ -24,11 +24,11 @@ class SortCommand(Command):
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(SortCommand, self).__init__(
super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
def execute(self):
if not super(SortCommand, self).execute():
if not super().execute():
return False
try:
......
......@@ -26,7 +26,7 @@ class TagCommand(Command):
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(TagCommand, self).__init__(
super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
self.force = False
......@@ -124,7 +124,7 @@ class TagCommand(Command):
self._print()
def execute(self):
if not super(TagCommand, self).execute():
if not super().execute():
return False
self._process_args()
......
......@@ -71,8 +71,8 @@ class Command(object):
""" Retrieves a value from the argument list at the given position. """
try:
return self.args[p_number]
except IndexError:
raise InvalidCommandArgument
except IndexError as ie:
raise InvalidCommandArgument from ie
def getopt(self, p_flags, p_long=None):
p_long = p_long or []
......
......@@ -354,10 +354,13 @@ class _Config:
alias_dict = dict()
for alias, meaning in aliases:
meaning = shlex.split(meaning)
real_subcommand = meaning[0]
alias_args = meaning[1:]
alias_dict[alias] = (real_subcommand, alias_args)
try:
meaning = shlex.split(meaning)
real_subcommand = meaning[0]
alias_args = meaning[1:]
alias_dict[alias] = (real_subcommand, alias_args)
except ValueError as verr:
alias_dict[alias] = str(verr)
return alias_dict
......@@ -404,7 +407,7 @@ def config(p_path=None, p_overrides=None):
try:
config.instance = _Config(p_path, p_overrides)
except configparser.ParsingError as perr:
raise ConfigError(str(perr))
raise ConfigError(str(perr)) from perr
return config.instance
......
......@@ -31,7 +31,7 @@ class DCommand(MultiCommand):
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(DCommand, self).__init__(
super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
self.force = False
......
......@@ -32,7 +32,7 @@ class ExpressionCommand(Command):
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(ExpressionCommand, self).__init__(
super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
self.sort_expression = config().sort_string()
......
......@@ -62,7 +62,7 @@ class GrepFilter(Filter):
""" Matches when the todo text contains a text. """
def __init__(self, p_expression, p_case_sensitive=None):
super(GrepFilter, self).__init__()
super().__init__()
# convert to string in case we receive integers
self.expression = p_expression
......@@ -96,13 +96,15 @@ class RelevanceFilter(Filter):
"""
def match(self, p_todo):
is_due = p_todo.is_active()
is_due |= p_todo.due_date() == None
is_due |= p_todo.priority() == 'A'
is_due |= p_todo.priority() == 'B' and p_todo.days_till_due() <= 30
is_due |= p_todo.priority() == 'C' and p_todo.days_till_due() <= 14
active = p_todo.is_active()
return p_todo.is_active() and is_due
is_due = active
is_due = is_due or p_todo.due_date() == None
is_due = is_due or p_todo.priority() == 'A'
is_due = is_due or (p_todo.priority() == 'B' and p_todo.days_till_due() <= 30)
is_due = is_due or (p_todo.priority() == 'C' and p_todo.days_till_due() <= 14)
return active and is_due
class DependencyFilter(Filter):
......@@ -115,7 +117,7 @@ class DependencyFilter(Filter):
Pass on a TodoList instance such that the dependencies can be
looked up.
"""
super(DependencyFilter, self).__init__()
super().__init__()
self.todolist = p_todolist
def match(self, p_todo):
......@@ -138,7 +140,7 @@ class InstanceFilter(Filter):
This is handy for constructing a view given a plain list of Todo items.
"""
super(InstanceFilter, self).__init__()
super().__init__()
self.todos = p_todos
def match(self, p_todo):
......@@ -154,7 +156,7 @@ class InstanceFilter(Filter):
class LimitFilter(Filter):
def __init__(self, p_limit):
super(LimitFilter, self).__init__()
super().__init__()
self.limit = p_limit
def filter(self, p_todos):
......@@ -167,7 +169,7 @@ class OrdinalFilter(Filter):
""" Base class for ordinal filters. """
def __init__(self, p_expression, p_pattern):
super(OrdinalFilter, self).__init__()
super().__init__()
self.expression = p_expression
......@@ -206,7 +208,7 @@ _ORDINAL_TAG_MATCH = r"(?P<key>[^:]*):" + _OPERATOR_MATCH + _VALUE_MATCH
class OrdinalTagFilter(OrdinalFilter):
def __init__(self, p_expression):
super(OrdinalTagFilter, self).__init__(p_expression, _ORDINAL_TAG_MATCH)
super().__init__(p_expression, _ORDINAL_TAG_MATCH)
def match(self, p_todo):
"""
......@@ -247,7 +249,7 @@ class OrdinalTagFilter(OrdinalFilter):
class _DateAttributeFilter(OrdinalFilter):
def __init__(self, p_expression, p_match, p_getter):
super(_DateAttributeFilter, self).__init__(p_expression, p_match)
super().__init__(p_expression, p_match)
self.getter = p_getter
def match(self, p_todo):
......@@ -268,7 +270,7 @@ _CREATED_MATCH = r'creat(ion|ed?):' + _OPERATOR_MATCH + _VALUE_MATCH
class CreationFilter(_DateAttributeFilter):
def __init__(self, p_expression):
super(CreationFilter, self).__init__(
super().__init__(
p_expression,
_CREATED_MATCH,
lambda t: t.creation_date() # pragma: no branch
......@@ -280,7 +282,7 @@ _COMPLETED_MATCH = r'complet(ed?|ion):' + _OPERATOR_MATCH + _VALUE_MATCH
class CompletionFilter(_DateAttributeFilter):
def __init__(self, p_expression):
super(CompletionFilter, self).__init__(
super().__init__(
p_expression,
_COMPLETED_MATCH,
lambda t: t.completion_date() # pragma: no branch
......@@ -292,7 +294,7 @@ _PRIORITY_MATCH = r"\(" + _OPERATOR_MATCH + r"(?P<value>[A-Z]{1})\)"
class PriorityFilter(OrdinalFilter):
def __init__(self, p_expression):
super(PriorityFilter, self).__init__(p_expression, _PRIORITY_MATCH)
super().__init__(p_expression, _PRIORITY_MATCH)
def match(self, p_todo):
"""
......
......@@ -66,7 +66,7 @@ class IcalPrinter(Printer):
"""
def __init__(self, p_todolist):
super(IcalPrinter, self).__init__()
super().__init__()
self.todolist = p_todolist
try:
......
......@@ -83,12 +83,14 @@ def average_importance(p_todo, p_ignore_weekend=config().ignore_weekends()):
average = 0
parents = []
if 'parents' in p_todo.attributes:
try:
sum_importance = own_importance
parents = p_todo.attributes['parents']
parents = p_todo.parents()
for parent in parents:
sum_importance += importance(parent, p_ignore_weekend)
average = float(sum_importance) / float(1 + len(parents))
except AttributeError:
pass
return max(own_importance, average)
......@@ -52,7 +52,7 @@ class JsonPrinter(Printer):
"""
def __init__(self):
super(JsonPrinter, self).__init__()
super().__init__()
def print_todo(self, p_todo):
return json.dumps(_convert_todo(p_todo), ensure_ascii=False,
......
......@@ -43,7 +43,7 @@ def humanize_date(p_datetime):
""" Returns a relative date string from a datetime object. """
now = arrow.now()
date = now.replace(day=p_datetime.day, month=p_datetime.month, year=p_datetime.year)
return date.humanize()
return date.humanize().replace('just now', 'today')
def humanize_dates(p_due=None, p_start=None, p_creation=None):
"""
......@@ -183,6 +183,9 @@ class ListFormatParser(object):
# priority (or placeholder space)
'P': lambda t: t.priority() if t.priority() else ' ',
# raw text
'r': lambda t: t.source(),
# text
's': lambda t: t.text(),
......
......@@ -27,7 +27,7 @@ class MultiCommand(ExpressionCommand):
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(MultiCommand, self).__init__(
super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
self.todos = []
......@@ -114,7 +114,7 @@ class MultiCommand(ExpressionCommand):
raise NotImplementedError
def execute(self):
if not super(MultiCommand, self).execute():
if not super().execute():
return False
self._process_flags()
......
......@@ -50,7 +50,7 @@ class PrettyPrinter(Printer):
"""
Constructor.
"""
super(PrettyPrinter, self).__init__()
super().__init__()
self.filters = []
def add_filter(self, p_filter):
......
......@@ -29,4 +29,3 @@ class PrettyPrinterFilter(object):
Applies a filter to p_todo_str and returns a modified version of it.
"""
raise NotImplementedError
......@@ -37,9 +37,26 @@ def _add_months(p_sourcedate, p_months):
return date(year, month, day)
def _add_business_days(p_sourcedate, p_bdays):
""" Adds a number of business days to the source date. """
result = p_sourcedate
delta = 1 if p_bdays > 0 else -1
while abs(p_bdays) > 0:
result += timedelta(delta)
weekday = result.weekday()
if weekday >= 5:
continue
p_bdays = p_bdays - 1 if delta > 0 else p_bdays + 1
return result
def _convert_pattern(p_length, p_periodunit, p_offset=None):
"""
Converts a pattern in the form [0-9][dwmy] and returns a date from the
Converts a pattern in the form [0-9][dwmyb] and returns a date from the
offset with the period of time added to it.
"""
result = None
......@@ -55,6 +72,8 @@ def _convert_pattern(p_length, p_periodunit, p_offset=None):
result = _add_months(p_offset, p_length)
elif p_periodunit == 'y':
result = _add_months(p_offset, p_length * 12)
elif p_periodunit == 'b':
result = _add_business_days(p_offset, p_length)
return result
......@@ -98,7 +117,8 @@ def relative_date_to_date(p_date, p_offset=None):
p_date = p_date.lower()
p_offset = p_offset or date.today()
relative = re.match('(?P<length>-?[0-9]+)(?P<period>[dwmy])$', p_date, re.I)
relative = re.match('(?P<length>-?[0-9]+)(?P<period>[dwmyb])$',
p_date, re.I)
monday = 'mo(n(day)?)?$'
tuesday = 'tu(e(sday)?)?$'
......
......@@ -48,17 +48,14 @@ class TodoBase(object):
Returns a tag value associated with p_key. Returns p_default if p_key
does not exist (which defaults to None).
"""
values = self.tag_values(p_key)
return values[0] if len(values) else p_default
return self.tag_values(p_key)[0] if p_key in self.fields['tags'] else p_default
def tag_values(self, p_key):
"""
Returns a list of all tag values associated with p_key. Returns
empty list if p_key does not exist.
"""
tags = self.fields['tags']
matches = [tag[1] for tag in tags if tag[0] == p_key]
return matches if len(matches) else []
return self.fields['tags'][p_key] if p_key in self.fields['tags'] else []
def has_tag(self, p_key, p_value=""):
"""
......@@ -66,14 +63,28 @@ class TodoBase(object):
value is passed, it will only return true when there exists a tag with
the given key-value combination.
"""
result = [t for t in self.tag_values(p_key)
if p_value == "" or t == p_value]
return len(result) > 0
tags = self.fields['tags']
return p_key in tags and (p_value == "" or p_value in tags[p_key])
def add_tag(self, p_key, p_value):
""" Adds a tag to the todo. """
self.set_tag(p_key, p_value, True)
def _remove_tag_helper(self, p_key, p_value):
"""
Removes a tag from the internal todo dictionary. Only those instances
with the given value are removed. If the value is empty, all tags with
the given key are removed.
"""
tags = self.fields['tags']
try:
tags[p_key] = [t for t in tags[p_key] if p_value != "" and t != p_value]
if len(tags[p_key]) == 0:
del tags[p_key]
except KeyError:
pass
def set_tag(self, p_key, p_value="", p_force_add=False, p_old_value=""):
"""
Sets a occurrence of the tag identified by p_key. Sets an arbitrary
......@@ -92,12 +103,11 @@ class TodoBase(object):
self.remove_tag(p_key, p_old_value)
return
tags = self.fields['tags']
value = p_old_value if p_old_value else self.tag_value(p_key)
if not p_force_add and value:
# remove old value from the tags
self.fields['tags'] = [t for t in self.fields['tags']
if not (t[0] == p_key and t[1] == value)]
self._remove_tag_helper(p_key, value)
self.src = re.sub(
r'\b' + p_key + ':' + value + r'\b',
......@@ -107,7 +117,10 @@ class TodoBase(object):
else:
self.src += ' ' + p_key + ':' + p_value
self.fields['tags'].append((p_key, p_value))
try:
tags[p_key].append(p_value)
except KeyError:
tags[p_key] = [p_value]
def remove_tag(self, p_key, p_value=""):
"""
......@@ -116,12 +129,7 @@ class TodoBase(object):
removed.
Else, only those tags with the value will be removed.
"""
# Build a new list that excludes the specified tag, match by value when
# p_value is given.
self.fields['tags'] = [t for t in self.fields['tags']
if not (t[0] == p_key and (p_value == "" or
t[1] == p_value))]
self._remove_tag_helper(p_key, p_value)
# when value == "", match any value having key p_key
value = p_value if p_value != "" else r'\S+'
......@@ -132,7 +140,8 @@ class TodoBase(object):
Returns a list of tuples with key-value pairs representing tags in
this todo item.
"""
return self.fields['tags']
tags = self.fields['tags']
return [(t, v) for t in tags for v in tags[t]]
def set_priority(self, p_priority):
"""
......
......@@ -18,11 +18,35 @@
A list of todo items.
"""
import types
from topydo.lib.Config import config
from topydo.lib.Graph import DirectedGraph
from topydo.lib.TodoListBase import TodoListBase
def _needs_dependencies(p_function):
"""
A decorator that triggers the population of the dependency tree in a
TodoList (and other administration). The decorator should be applied to
methods of TodoList that require dependency information.
"""
def build_dependency_information(p_todolist):
for todo in p_todolist._todos:
p_todolist._register_todo(todo)
def inner(self, *args, **kwargs):
if not self._initialized:
self._initialized = True
from topydo.lib.Graph import DirectedGraph
self._depgraph = DirectedGraph()
build_dependency_information(self)
return p_function(self, *args, **kwargs)
return inner
class TodoList(TodoListBase):
"""
Provides operations for a todo list, such as adding items, removing them,
......@@ -37,21 +61,27 @@ class TodoList(TodoListBase):
Should be given a list of strings, each element a single todo string.
The string will be parsed.
"""
self._initialized = False # whether dependency information was
# initialized
# initialize these first because the constructor calls add_list
self._tododict = {} # hash(todo) to todo lookup
self._depgraph = DirectedGraph()
self._parentdict = {} # dependency id => parent todo
self._depgraph = None
super(TodoList, self).__init__(p_todostrings)
super().__init__(p_todostrings)
@_needs_dependencies
def todo_by_dep_id(self, p_dep_id):
"""
Returns the todo that has the id tag set to the value p_dep_id.
There is only one such task, the behavior is undefined when a tag has
more than one id tag.
There is only one such task, the behavior is undefined when a todo item
has more than one id tag.
"""
hits = [t for t in self._todos if t.tag_value('id') == p_dep_id]
return hits[0] if len(hits) else None
try:
return self._parentdict[p_dep_id]
except KeyError:
return None
def _maintain_dep_graph(self, p_todo):
"""
......@@ -61,6 +91,7 @@ class TodoList(TodoListBase):
dep_id = p_todo.tag_value('id')
# maintain dependency graph
if dep_id:
self._parentdict[dep_id] = p_todo
self._depgraph.add_node(hash(p_todo))
# connect all tasks we have in memory so far that refer to this
......@@ -70,31 +101,41 @@ class TodoList(TodoListBase):
self._depgraph.add_edge(hash(p_todo), hash(dep), dep_id)
for child in p_todo.tag_values('p'):
parent = self.todo_by_dep_id(child)
if parent:
self._depgraph.add_edge(hash(parent), hash(p_todo), child)
for dep_id in p_todo.tag_values('p'):
try:
parent = self._parentdict[dep_id]
self._depgraph.add_edge(hash(parent), hash(p_todo), dep_id)
except KeyError:
pass
def _register_todo(self, p_todo):
self._maintain_dep_graph(p_todo)
self._tododict[hash(p_todo)] = p_todo
def add_todos(self, p_todos):
for todo in p_todos:
self._todos.append(todo)
self._tododict[hash(todo)] = todo
self._maintain_dep_graph(todo)
super().add_todos(p_todos)
self._update_todo_ids()
self._update_parent_cache()
self.dirty = True
for todo in self._todos:
todo.parents = types.MethodType(lambda i: self.parents(i), todo)
# only do administration when the dependency info is initialized,
# otherwise we postpone it until it's really needed (through the
# _needs_dependencies decorator)
if self._initialized:
self._register_todo(todo)
def delete(self, p_todo):
""" Deletes a todo item from the list. """
try:
number = self._todos.index(p_todo)
for child in self.children(p_todo):
self.remove_dependency(p_todo, child)
if p_todo.has_tag('id'):
for child in self.children(p_todo):
self.remove_dependency(p_todo, child)
for parent in self.parents(p_todo):
self.remove_dependency(parent, p_todo)
if p_todo.has_tag('p'):
for parent in self.parents(p_todo):
self.remove_dependency(parent, p_todo)
del self._todos[number]
self._update_todo_ids()
......@@ -104,6 +145,7 @@ class TodoList(TodoListBase):
# todo item couldn't be found, ignore
pass
@_needs_dependencies
def add_dependency(self, p_from_todo, p_to_todo):
""" Adds a dependency from task 1 to task 2. """
def find_next_id():
......@@ -117,7 +159,8 @@ class TodoList(TodoListBase):
Returns True if there exists a todo with the given parent ID.
"""
for todo in self._todos:
if todo.has_tag('id', str(p_id)):
number = str(p_id)
if todo.has_tag('id', number) or todo.has_tag('p', number):
return True
return False
......@@ -158,11 +201,11 @@ class TodoList(TodoListBase):
p_to_todo.add_tag('p', dep_id)
self._depgraph.add_edge(hash(p_from_todo), hash(p_to_todo), dep_id)
self._update_parent_cache()
append_projects_to_subtodo()
append_contexts_to_subtodo()
self.dirty = True
@_needs_dependencies
def remove_dependency(self, p_from_todo, p_to_todo):
""" Removes a dependency between two todos. """
dep_id = p_from_todo.tag_value('id')
......@@ -170,13 +213,14 @@ class TodoList(TodoListBase):
if dep_id:
p_to_todo.remove_tag('p', dep_id)
self._depgraph.remove_edge(hash(p_from_todo), hash(p_to_todo))
self._update_parent_cache()
if not self.children(p_from_todo, True):
p_from_todo.remove_tag('id')
del self._parentdict[dep_id]
self.dirty = True
@_needs_dependencies
def parents(self, p_todo, p_only_direct=False):
"""
Returns a list of parent todos that (in)directly depend on the
......@@ -186,6 +230,7 @@ class TodoList(TodoListBase):
hash(p_todo), not p_only_direct)
return [self._tododict[parent] for parent in parents]
@_needs_dependencies
def children(self, p_todo, p_only_direct=False):
"""
Returns a list of child todos that the given todo (in)directly depends
......@@ -195,6 +240,7 @@ class TodoList(TodoListBase):
self._depgraph.outgoing_neighbors(hash(p_todo), not p_only_direct)
return [self._tododict[child] for child in children]
@_needs_dependencies
def clean_dependencies(self):
"""
Cleans the dependency graph.
......@@ -219,6 +265,7 @@ class TodoList(TodoListBase):
value = todo.tag_value('id')
if not self._depgraph.has_edge_id(value):
remove_tag(todo, 'id', value)
del self._parentdict[value]
def clean_orphan_relations():
"""
......@@ -237,12 +284,3 @@ class TodoList(TodoListBase):
clean_parent_relations()
clean_orphan_relations()
def _update_parent_cache(self):
"""
Sets the attribute to the list of parents, such that others may access
it outside this todo list.
This is used for calculating the average importance, that requires
access to a todo's parents.
"""
for todo in self._todos:
todo.attributes['parents'] = self.parents(todo)
......@@ -54,6 +54,12 @@ class TodoListBase(object):
self.add_list(p_todostrings)
self._dirty = False
def __iter__(self):
"""
Allows use of `for my_todo in todolist` constructs.
"""
return iter(self._todos)
def todo(self, p_identifier):
"""
The _todos list has the same order as in the backend store (usually
......@@ -95,11 +101,11 @@ class TodoListBase(object):
# the expression is a string and no leading zeroes,
# treat it as an integer
raise TypeError
except TypeError:
except TypeError as te:
try:
result = self._todos[int(p_identifier) - 1]
except IndexError:
raise InvalidTodoException
except (ValueError, IndexError):
raise InvalidTodoException from te
return result
......@@ -251,8 +257,8 @@ class TodoListBase(object):
return self._todo_id_map[p_todo]
else:
return self._todos.index(p_todo) + 1
except (ValueError, KeyError):
raise InvalidTodoException
except (ValueError, KeyError) as ex:
raise InvalidTodoException from ex
def _update_todo_ids(self):
# the idea is to have a hash that is independent of the position of the
......
......@@ -33,7 +33,7 @@ _NORMAL_HEAD_MATCH = re.compile(
r'(\((?P<priority>[A-Z])\) )?' + '((?P<creationDate>' + _DATE_MATCH +
') )?(?P<rest>.*)')
_TAG_MATCH = re.compile('(?P<key>[^:]+):(?P<value>.+)')
_TAG_MATCH = re.compile('(?P<tag>[^:]+):(?P<value>.+)')
_PROJECT_MATCH = re.compile(r'\+(\S*\w)')
_CONTEXT_MATCH = re.compile(r'@(\S*\w)')
......@@ -57,7 +57,7 @@ def parse_line(p_string):
'text': "",
'projects': [],
'contexts': [],
'tags': []
'tags': {},
}
completed_head = _COMPLETED_HEAD_MATCH.match(p_string)
......@@ -94,10 +94,14 @@ def parse_line(p_string):
tag = _TAG_MATCH.match(word)
if tag:
result['tags'].append((tag.group('key'), tag.group('value')))
continue
result['text'] += word + ' '
tag_name = tag.group('tag')
tag_value = tag.group('value')
try:
result['tags'][tag_name].append(tag_value)
except KeyError:
result['tags'][tag_name] = [tag_value]
else:
result['text'] += word + ' '
# strip trailing space from resulting text
result['text'] = result['text'][:-1]
......
......@@ -87,7 +87,7 @@ def get_terminal_size(p_getter=None):
occurs during running the unittest on Windows (but not on Linux?)
"""
terminal_size = namedtuple('Terminal_Size', 'columns lines')
sz = terminal_size((80, 24))
sz = terminal_size(80, 24)
return sz
......
......@@ -25,7 +25,7 @@ from topydo.lib.ListFormat import ListFormatParser
class PrettyPrinterFormatFilter(PrettyPrinterFilter):
def __init__(self, p_todolist, p_format=None):
super(PrettyPrinterFormatFilter, self).__init__()
super().__init__()
self.parser = ListFormatParser(p_todolist, p_format)
def filter(self, p_todo_str, p_todo):
......
......@@ -23,7 +23,7 @@ class PrettyPrinterNumbers(PrettyPrinterFilter):
""" Prepends the todo's number, retrieved from the todolist. """
def __init__(self, p_todolist):
super(PrettyPrinterNumbers, self).__init__()
super().__init__()
self.todolist = p_todolist
def filter(self, p_todo_str, p_todo):
......
......@@ -28,7 +28,7 @@ from topydo.ui.KeystateWidget import KeystateWidget
from topydo.ui.TodoListWidget import TodoListWidget
from topydo.ui.ViewWidget import ViewWidget
from topydo.ui.ColumnLayout import columns
from topydo.lib.Config import config
from topydo.lib.Config import config, ConfigError
from topydo.lib.Sorter import Sorter
from topydo.lib.Filter import get_filter_list, RelevanceFilter, DependencyFilter
from topydo.lib.Utils import get_terminal_size
......@@ -171,7 +171,12 @@ class UIApplication(CLIApplicationBase):
"""
p_output = p_output or self._output
p_command = shlex.split(p_command)
(subcommand, args) = get_subcommand(p_command)
try:
(subcommand, args) = get_subcommand(p_command)
except ConfigError as cerr:
self._print_to_console(
'Error: {}. Check your aliases configuration.'.format(cerr))
return
self._backup(subcommand, args)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment