Commit 851e2cdd authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge branch 'master' into dot

Revived the dot subcommand and the dot printer for 'ls'.
parents f2072530 798ed50b
...@@ -14,3 +14,4 @@ exclude_lines = ...@@ -14,3 +14,4 @@ exclude_lines =
omit = omit =
topydo/commands/ExitCommand.py topydo/commands/ExitCommand.py
topydo/lib/Version.py topydo/lib/Version.py
topydo/ui/*
...@@ -8,8 +8,9 @@ python: ...@@ -8,8 +8,9 @@ python:
install: install:
- "python -m pip install pip --upgrade" - "python -m pip install pip --upgrade"
- "pip install ." - "pip install ."
- "pip install .[columns]"
- "pip install .[ical]" - "pip install .[ical]"
- "pip install .[prompt-toolkit]" - "pip install .[prompt]"
- "pip install .[test]" - "pip install .[test]"
- "pip install pylint" - "pip install pylint"
- "pip install codecov" - "pip install codecov"
......
0.10
----
A major release, introducing a new user interface (TUI). Special thanks go to
@mruwek for helping out to get this UI in its current shape.
* New: A column-based user interface. Each column has its own filters and sort
order, allowing you to build a dashboard with your todo items. Launch with
`topydo columns`.
* New: color blocks that change from green to red (overdue) as time passes by.
Use the %z placeholder to add color blocks to the `ls` output.
* New: color option can be set to 0, 1, 16 or 256 and if needed overridden by
-C on the commandline.
* New: recurrence based on business days. Skips Saturdays and Sundays when
calculating the next date (thanks to @mruwek).
* New: items can be sorted by length (use 'length' as sort field).
* New: parents-of and children-of operators with `add`, `dep` and `append`
subcommands. The todo item receives the same parents/children from the
specified todo item.
* New: `append` understands relative dates and other tags that are special to
`add` (thanks to @rameshg87).
* Fix: dependency ID creation with orphan todo items.
* Fix: crash after completing/deleting an edited item.
* Fix: crash after completing an item that got a new dependency with `dep add`
* Change: a new tag value with an existing key can be added with the tag
subcommand (thanks to @MinchinWeb)
* Change: ~/.config/topydo/config can be used as a configuration file.
* Change: No backups are written for read-only commands (e.g. lsprj)
* Change: topydo is more scalable for large todo.txt files.
* Known issue: color blocks are not shown in `ls` output in the column UI.
0.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
* `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).
* `ls -N` prints enough todo items such that it fits on one screen (thanks to
@MinchinWeb).
* Aliases can have a `{}` placeholder which is substituted with the alias'
arguments (thanks to @mruwek).
* `pri` accepts priorities in lowercase (thanks to @MinchinWeb).
* Several bugfixes for `dep gc`.
* Various test/CI improvements.
0.8 0.8
--- ---
...@@ -21,8 +75,8 @@ for the majority of these new features. ...@@ -21,8 +75,8 @@ for the majority of these new features.
* `ls` output can be customized with a -F flag or a configuration option: * `ls` output can be customized with a -F flag or a configuration option:
[ls] [ls]
list_format = |%I| %x %{(}p{)} %c %s %k %{due:}d %{t:}t list_format = |%I| %x %{(}p{)} %c %s %k %{due:}d %{t:}t
or `ls -F "%{(}p{)} %s %{due:}d"`. or `ls -F "%{(}p{)} %s %{due:}d"`.
......
...@@ -43,10 +43,6 @@ smoothly into topydo. ...@@ -43,10 +43,6 @@ smoothly into topydo.
exit 1 exit 1
fi fi
if ! python2 -m pylint --errors-only topydo test; then
exit 1
fi
if ! python3 -m pylint --errors-only topydo test; then if ! python3 -m pylint --errors-only topydo test; then
exit 1 exit 1
fi fi
......
...@@ -26,12 +26,26 @@ Simply install with: ...@@ -26,12 +26,26 @@ Simply install with:
pip install topydo 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 * [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 * [prompt-toolkit][6] : For topydo's _prompt_ mode, which offers a shell-like
interface with auto-completion. 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.
* [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 for PyPy3.
* [mock][11] : Used for testing. This was added to the standard
library in Python 3.3.
Demo Demo
---- ----
...@@ -46,3 +60,8 @@ Demo ...@@ -46,3 +60,8 @@ Demo
[5]: https://raw.githubusercontent.com/bram85/topydo/master/doc/topydo.gif [5]: https://raw.githubusercontent.com/bram85/topydo/master/doc/topydo.gif
[6]: https://github.com/jonathanslenders/python-prompt-toolkit [6]: https://github.com/jonathanslenders/python-prompt-toolkit
[7]: https://github.com/collective/icalendar [7]: https://github.com/collective/icalendar
[8]: https://github.com/crsmithdev/arrow
[9]: https://github.com/chrippa/backports.shutil_get_terminal_size
[10]: https://dateutil.readthedocs.org/
[11]: https://github.com/testing-cabal/mock
[12]: https://github.com/urwid/urwid
...@@ -33,12 +33,15 @@ setup( ...@@ -33,12 +33,15 @@ setup(
], ],
extras_require = { extras_require = {
':sys_platform=="win32"': ['colorama>=0.2.5'], ':sys_platform=="win32"': ['colorama>=0.2.5'],
':python_version=="3.2"': ['backports.shutil_get_terminal_size>=1.0.0'],
'columns': ['urwid >= 1.3.0'],
'ical': ['icalendar'], 'ical': ['icalendar'],
'prompt-toolkit': ['prompt-toolkit >= 0.53'], 'prompt': ['prompt_toolkit >= 0.53'],
'test': ['coverage', 'freezegun', 'green', ], 'test': ['coverage', 'freezegun', 'green', ],
'test:python_version=="3.2"': ['mock'],
}, },
entry_points= { entry_points= {
'console_scripts': ['topydo = topydo.cli.UILoader:main'], 'console_scripts': ['topydo = topydo.ui.UILoader:main'],
}, },
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
......
...@@ -20,14 +20,19 @@ from topydo.lib.Utils import escape_ansi ...@@ -20,14 +20,19 @@ from topydo.lib.Utils import escape_ansi
class CommandTest(TopydoTest): class CommandTest(TopydoTest):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CommandTest, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.output = "" self.output = ""
self.errors = "" self.errors = ""
def out(self, p_output): def out(self, p_output):
if p_output: if isinstance(p_output, list) and p_output:
self.output += escape_ansi(p_output + "\n") self.output += escape_ansi(
"\n".join([str(s) for s in p_output]) + "\n")
elif p_output:
self.output += str(p_output) + "\n"
def error(self, p_error): def error(self, p_error):
if p_error: if isinstance(p_error, list) and p_error:
self.errors += escape_ansi(p_error + "\n") self.errors += escape_ansi(p_error + "\n") + "\n"
elif p_error:
self.errors += str(p_error) + "\n"
[column_keymap]
up = up
<Left> = prev_column
<Esc>d = delete_column
(A) item 1
(B) item 2
(C) item 3
(D) item 4
(E) item 5
(F) item 6
(G) item 7
(H) item 8
(I) item 9
(J) item 10
(K) item 11
(L) item 12
(M) item 13
(N) item 14
(O) item 15
(P) item 16
(Q) item 17
(R) item 18
(S) item 19
(T) item 20
(U) item 21
(V) item 22
(W) item 23
(X) item 24
(Y) item 25
(Z) item 26
(A) item 27
(B) item 28
(C) item 29
(D) item 30
(E) item 31
(F) item 32
(G) item 33
(H) item 34
(I) item 35
(J) item 36
(K) item 37
(L) item 38
(M) item 39
(N) item 40
(O) item 41
(P) item 42
(Q) item 43
(R) item 44
(S) item 45
(T) item 46
(U) item 47
(V) item 48
(W) item 49
(X) item 50
1 No length (zero)
2016-04-25 2 No length (zero)
3 Length of 0 days t:2016-04-25 due:2016-04-25
4 Length of 0 days t:2016-04-25 due:2016-04-25
5 Length of 0 days tomorrow t:2016-04-26 due:2016-04-26
2016-04-25 1 Length of 1 day with creation due:2016-04-26
2 Length of 1 day t:2016-04-25 due:2016-04-26
2016-04-25 Length of 2 days with creation due:2016-04-27
4 Length of 0 days t:2016-04-25 due:2016-04-25
2016-04-25 2 No length (zero)
3 Length of 0 days t:2016-04-25 due:2016-04-25
5 Length of 0 days tomorrow t:2016-04-26 due:2016-04-26
2 Length of 1 day t:2016-04-25 due:2016-04-26
2016-04-25 Length of 2 days with creation due:2016-04-27
2016-04-25 1 Length of 1 day with creation due:2016-04-26
1 No length (zero)
...@@ -3,3 +3,5 @@ foo = rm -f test ...@@ -3,3 +3,5 @@ foo = rm -f test
baz = FooBar baz = FooBar
format = ls -F "|I| x c d {(}p{)} s k" -n 25 format = ls -F "|I| x c d {(}p{)} s k" -n 25
smile = ls smile = ls
star = tag {} star 1
quot = lol'd
...@@ -52,4 +52,4 @@ def todolist_to_string(p_list): ...@@ -52,4 +52,4 @@ def todolist_to_string(p_list):
def print_view(p_view): def print_view(p_view):
printer = PrettyPrinter() printer = PrettyPrinter()
return printer.print_list(p_view.todos) return "\n".join([str(s) for s in printer.print_list(p_view.todos)])
...@@ -33,7 +33,7 @@ except ImportError: ...@@ -33,7 +33,7 @@ except ImportError:
class AddCommandTest(CommandTest): class AddCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(AddCommandTest, self).setUp() super().setUp()
self.todolist = TodoList.TodoList([]) self.todolist = TodoList.TodoList([])
self.today = date.today().isoformat() self.today = date.today().isoformat()
...@@ -268,6 +268,61 @@ class AddCommandTest(CommandTest): ...@@ -268,6 +268,61 @@ class AddCommandTest(CommandTest):
self.assertEqual(self.output, "|wb3| {today} Bar p:1 @Context\n|wb3| {today} Bar @Context\n".format(today=self.today)) self.assertEqual(self.output, "|wb3| {today} Bar p:1 @Context\n|wb3| {today} Bar @Context\n".format(today=self.today))
def add_parentsof_helper(self, p_tag):
command = AddCommand.AddCommand(["Foo"], self.todolist, self.out,
self.error)
command.execute()
command = AddCommand.AddCommand(["Bar before:1"], self.todolist,
self.out, self.error)
command.execute()
command = AddCommand.AddCommand(["Baz {}:2".format(p_tag)],
self.todolist, self.out, self.error)
command.execute()
self.assertTrue(self.todolist.todo(3).has_tag('p', '1'))
def test_add_dep_parentsof01(self):
self.add_parentsof_helper('parentsof')
def test_add_dep_parentsof02(self):
self.add_parentsof_helper('parentof')
def test_add_dep_parentsof03(self):
self.add_parentsof_helper('parents-of')
def test_add_dep_parentsof04(self):
self.add_parentsof_helper('parent-of')
def add_childrenof_helper(self, p_tag):
command = AddCommand.AddCommand(["Foo"], self.todolist, self.out,
self.error)
command.execute()
command = AddCommand.AddCommand(["Bar before:1"], self.todolist,
self.out, self.error)
command.execute()
command = AddCommand.AddCommand(["Baz {}:1".format(p_tag)],
self.todolist, self.out, self.error)
command.execute()
self.assertTrue(self.todolist.todo(3).has_tag('id', '2'))
self.assertTrue(self.todolist.todo(2).has_tag('p', '2'))
def test_add_dep_childrenof01(self):
self.add_childrenof_helper('childrenof')
def test_add_dep_childrenof02(self):
self.add_childrenof_helper('childof')
def test_add_dep_childrenof03(self):
self.add_childrenof_helper('children-of')
def test_add_dep_childrenof04(self):
self.add_childrenof_helper('child-of')
def test_add_reldate1(self): def test_add_reldate1(self):
command = AddCommand.AddCommand(["Foo due:today"], self.todolist, command = AddCommand.AddCommand(["Foo due:today"], self.todolist,
self.out, self.error) self.out, self.error)
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest import unittest
from datetime import date
from test.command_testcase import CommandTest from test.command_testcase import CommandTest
from topydo.commands.AppendCommand import AppendCommand from topydo.commands.AppendCommand import AppendCommand
...@@ -23,9 +24,10 @@ from topydo.lib.TodoList import TodoList ...@@ -23,9 +24,10 @@ from topydo.lib.TodoList import TodoList
class AppendCommandTest(CommandTest): class AppendCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(AppendCommandTest, self).setUp() super().setUp()
self.todolist = TodoList([]) self.todolist = TodoList([])
self.todolist.add("Foo") self.todolist.add("Foo")
self.today = date.today().isoformat()
def test_append1(self): def test_append1(self):
command = AppendCommand([1, "Bar"], self.todolist, self.out, command = AppendCommand([1, "Bar"], self.todolist, self.out,
...@@ -79,6 +81,27 @@ class AppendCommandTest(CommandTest): ...@@ -79,6 +81,27 @@ class AppendCommandTest(CommandTest):
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, command.usage() + "\n") self.assertEqual(self.errors, command.usage() + "\n")
def test_append8(self):
command = AppendCommand([1, "due:today t:today"], self.todolist,
self.out, self.error)
command.execute()
self.assertEqual(self.output,
"| 1| Foo due:%s t:%s\n" % (self.today, self.today))
self.assertEqual(self.errors, "")
def test_append9(self):
self.todolist.add("Qux due:2015-12-21 t:2015-12-21 before:1")
self.todolist.add("Baz")
command = AppendCommand([2, "due:today t:today before:3"], self.todolist,
self.out, self.error)
command.execute()
self.assertEqual(
self.output,
"| 2| Qux due:%s t:%s p:1 p:2\n" % (self.today, self.today))
self.assertEqual(self.errors, "")
def test_help(self): def test_help(self):
command = AppendCommand(["help"], self.todolist, self.out, self.error) command = AppendCommand(["help"], self.todolist, self.out, self.error)
command.execute() command.execute()
......
...@@ -30,8 +30,8 @@ class ArchiveCommandTest(CommandTest): ...@@ -30,8 +30,8 @@ class ArchiveCommandTest(CommandTest):
command = ArchiveCommand(todolist, archive) command = ArchiveCommand(todolist, archive)
command.execute() command.execute()
self.assertTrue(todolist.is_dirty()) self.assertTrue(todolist.dirty)
self.assertTrue(archive.is_dirty()) self.assertTrue(archive.dirty)
self.assertEqual(todolist.print_todos(), "x Not complete\n(C) Active") self.assertEqual(todolist.print_todos(), "x Not complete\n(C) Active")
self.assertEqual(archive.print_todos(), "x 2014-10-19 Complete\nx 2014-10-20 Another one complete") self.assertEqual(archive.print_todos(), "x 2014-10-19 Complete\nx 2014-10-20 Another one complete")
......
...@@ -19,152 +19,159 @@ ...@@ -19,152 +19,159 @@
import unittest import unittest
from test.topydo_testcase import TopydoTest from test.topydo_testcase import TopydoTest
from topydo.lib.Colors import NEUTRAL_COLOR, Colors from topydo.lib.Color import Color
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib.Todo import Todo
NEUTRAL_COLOR = '\033[0m'
class ColorsTest(TopydoTest): class ColorsTest(TopydoTest):
def test_project_color1(self): def test_project_color1(self):
config(p_overrides={('colorscheme', 'project_color'): '2'}) config(p_overrides={('colorscheme', 'project_color'): '2'})
color = Colors().get_project_color() self.assertEqual(config().project_color().as_ansi(p_decoration='bold'), '\033[1;32m')
self.assertEqual(color, '\033[1;38;5;2m')
def test_project_color2(self): def test_project_color2(self):
config(p_overrides={('colorscheme', 'project_color'): 'Foo'}) config(p_overrides={('colorscheme', 'project_color'): 'Foo'})
color = Colors().get_project_color() self.assertEqual(config().project_color().as_ansi(p_decoration='bold'), NEUTRAL_COLOR)
self.assertEqual(color, NEUTRAL_COLOR)
def test_project_color3(self): def test_project_color3(self):
config(p_overrides={('colorscheme', 'project_color'): 'yellow'}) config(p_overrides={('colorscheme', 'project_color'): 'yellow'})
color = Colors().get_project_color() self.assertEqual(config().project_color().as_ansi(p_decoration='bold'), '\033[1;33m')
self.assertEqual(color, '\033[1;33m')
def test_project_color4(self): def test_project_color4(self):
config(p_overrides={('colorscheme', 'project_color'): '686'}) config(p_overrides={('colorscheme', 'project_color'): '686'})
color = Colors().get_project_color() self.assertEqual(config().project_color().as_ansi(p_decoration='bold'), NEUTRAL_COLOR)
self.assertEqual(color, NEUTRAL_COLOR)
def test_context_color1(self): def test_context_color1(self):
config(p_overrides={('colorscheme', 'context_color'): '35'}) config(p_overrides={('colorscheme', 'context_color'): '35'})
color = Colors().get_context_color() self.assertEqual(config().context_color().as_ansi(p_decoration='bold'), '\033[1;38;5;35m')
self.assertEqual(color, '\033[1;38;5;35m')
def test_context_color2(self): def test_context_color2(self):
config(p_overrides={('colorscheme', 'context_color'): 'Bar'}) config(p_overrides={('colorscheme', 'context_color'): 'Bar'})
color = Colors().get_context_color() self.assertEqual(config().context_color().as_ansi(p_decoration='bold'), NEUTRAL_COLOR)
self.assertEqual(color, NEUTRAL_COLOR)
def test_context_color3(self): def test_context_color3(self):
config(p_overrides={('colorscheme', 'context_color'): 'magenta'}) config(p_overrides={('colorscheme', 'context_color'): 'magenta'})
color = Colors().get_context_color() self.assertEqual(config().context_color().as_ansi(p_decoration='bold'), '\033[1;35m')
self.assertEqual(color, '\033[1;35m')
def test_context_color4(self): def test_context_color4(self):
config(p_overrides={('colorscheme', 'context_color'): '392'}) config(p_overrides={('colorscheme', 'context_color'): '392'})
color = Colors().get_context_color() self.assertEqual(config().context_color().as_ansi(p_decoration='bold'), NEUTRAL_COLOR)
self.assertEqual(color, NEUTRAL_COLOR)
def test_metadata_color1(self): def test_metadata_color1(self):
config(p_overrides={('colorscheme', 'metadata_color'): '128'}) config(p_overrides={('colorscheme', 'metadata_color'): '128'})
color = Colors().get_metadata_color() self.assertEqual(config().metadata_color().as_ansi(p_decoration='bold'), '\033[1;38;5;128m')
self.assertEqual(color, '\033[1;38;5;128m')
def test_metadata_color2(self): def test_metadata_color2(self):
config(p_overrides={('colorscheme', 'metadata_color'): 'Baz'}) config(p_overrides={('colorscheme', 'metadata_color'): 'Baz'})
color = Colors().get_metadata_color() self.assertEqual(config().metadata_color().as_ansi(p_decoration='bold'), NEUTRAL_COLOR)
self.assertEqual(color, NEUTRAL_COLOR)
def test_metadata_color3(self): def test_metadata_color3(self):
config(p_overrides={('colorscheme', 'metadata_color'): 'light-red'}) config(p_overrides={('colorscheme', 'metadata_color'): 'light-red'})
color = Colors().get_metadata_color() self.assertEqual(config().metadata_color().as_ansi(p_decoration='bold'), '\033[1;1;31m')
self.assertEqual(color, '\033[1;1;31m')
def test_metadata_color4(self): def test_metadata_color4(self):
config(p_overrides={('colorscheme', 'metadata_color'): '777'}) config(p_overrides={('colorscheme', 'metadata_color'): '777'})
color = Colors().get_metadata_color() self.assertEqual(config().metadata_color().as_ansi(p_decoration='bold'), NEUTRAL_COLOR)
self.assertEqual(color, NEUTRAL_COLOR)
def test_link_color1(self): def test_link_color1(self):
config(p_overrides={('colorscheme', 'link_color'): '77'}) config(p_overrides={('colorscheme', 'link_color'): '77'})
color = Colors().get_link_color() self.assertEqual(config().link_color().as_ansi(p_decoration='underline'), '\033[4;38;5;77m')
self.assertEqual(color, '\033[4;38;5;77m')
def test_link_color2(self): def test_link_color2(self):
config(p_overrides={('colorscheme', 'link_color'): 'FooBar'}) config(p_overrides={('colorscheme', 'link_color'): 'FooBar'})
color = Colors().get_link_color() self.assertEqual(config().link_color().as_ansi(p_decoration='underline'), NEUTRAL_COLOR)
self.assertEqual(color, NEUTRAL_COLOR)
def test_link_color3(self): def test_link_color3(self):
config(p_overrides={('colorscheme', 'link_color'): 'red'}) config(p_overrides={('colorscheme', 'link_color'): 'red'})
color = Colors().get_link_color() self.assertEqual(config().link_color().as_ansi(p_decoration='underline'), '\033[4;31m')
self.assertEqual(color, '\033[4;31m')
def test_link_color4(self): def test_link_color4(self):
config(p_overrides={('colorscheme', 'link_color'): '777'}) config(p_overrides={('colorscheme', 'link_color'): '777'})
color = Colors().get_link_color() self.assertEqual(config().link_color().as_ansi(p_decoration='underline'), NEUTRAL_COLOR)
self.assertEqual(color, NEUTRAL_COLOR)
def test_priority_color1(self): def test_priority_color1(self):
config("test/data/ColorsTest1.conf") config("test/data/ColorsTest1.conf")
colors = Colors() todo_a = Todo('(A) Foo')
todo_b = Todo('(B) Bar')
todo_c = Todo('(C) FooBar')
self.assertEqual(colors.get_priority_color('A'), '\033[0;38;5;1m') color_a = config().priority_color(todo_a.priority()).as_ansi()
self.assertEqual(colors.get_priority_color('B'), '\033[0;38;5;2m') color_b = config().priority_color(todo_b.priority()).as_ansi()
self.assertEqual(colors.get_priority_color('C'), '\033[0;38;5;3m') color_c = config().priority_color(todo_c.priority()).as_ansi()
self.assertEqual(color_a, '\033[0;31m')
self.assertEqual(color_b, '\033[0;32m')
self.assertEqual(color_c, '\033[0;33m')
def test_priority_color2(self): def test_priority_color2(self):
config("test/data/ColorsTest2.conf") config("test/data/ColorsTest2.conf")
colors = Colors() todo_a = Todo('(A) Foo')
todo_b = Todo('(B) Bar')
todo_c = Todo('(C) FooBar')
color_a = config().priority_color(todo_a.priority()).as_ansi()
color_b = config().priority_color(todo_b.priority()).as_ansi()
color_c = config().priority_color(todo_c.priority()).as_ansi()
self.assertEqual(colors.get_priority_color('A'), '\033[0;35m') self.assertEqual(color_a, '\033[0;35m')
self.assertEqual(colors.get_priority_color('B'), '\033[0;1;36m') self.assertEqual(color_b, '\033[0;1;36m')
self.assertEqual(colors.get_priority_color('C'), '\033[0;37m') self.assertEqual(color_c, '\033[0;37m')
def test_priority_color3(self): def test_priority_color3(self):
config("test/data/ColorsTest3.conf") config("test/data/ColorsTest3.conf")
colors = Colors() todo_a = Todo('(A) Foo')
todo_b = Todo('(B) Bar')
self.assertEqual(colors.get_priority_color('A'), '\033[0;35m') todo_z = Todo('(Z) FooBar')
self.assertEqual(colors.get_priority_color('B'), '\033[0;1;36m') todo_d = Todo('(D) Baz')
self.assertEqual(colors.get_priority_color('Z'), NEUTRAL_COLOR) todo_c = Todo('(C) FooBaz')
self.assertEqual(colors.get_priority_color('D'), '\033[0;31m')
self.assertEqual(colors.get_priority_color('C'), '\033[0;38;5;7m') color_a = config().priority_color(todo_a.priority()).as_ansi()
color_b = config().priority_color(todo_b.priority()).as_ansi()
color_z = config().priority_color(todo_z.priority()).as_ansi()
color_d = config().priority_color(todo_d.priority()).as_ansi()
color_c = config().priority_color(todo_c.priority()).as_ansi()
self.assertEqual(color_a, '\033[0;35m')
self.assertEqual(color_b, '\033[0;1;36m')
self.assertEqual(color_z, NEUTRAL_COLOR)
self.assertEqual(color_d, '\033[0;31m')
self.assertEqual(color_c, '\033[0;37m')
def test_priority_color4(self): def test_priority_color4(self):
config("test/data/ColorsTest4.conf") config("test/data/ColorsTest4.conf")
colors = Colors() todo_a = Todo('(A) Foo')
todo_b = Todo('(B) Bar')
todo_c = Todo('(C) FooBar')
self.assertEqual(colors.get_priority_color('A'), NEUTRAL_COLOR) color_a = config().priority_color(todo_a.priority()).as_ansi()
self.assertEqual(colors.get_priority_color('B'), NEUTRAL_COLOR) color_b = config().priority_color(todo_b.priority()).as_ansi()
self.assertEqual(colors.get_priority_color('C'), NEUTRAL_COLOR) color_c = config().priority_color(todo_c.priority()).as_ansi()
self.assertEqual(color_a, '')
self.assertEqual(color_b, '')
self.assertEqual(color_c, '')
def test_empty_color_values(self): def test_empty_color_values(self):
config("test/data/ColorsTest5.conf") config("test/data/ColorsTest5.conf")
colors = Colors() project_color = config().project_color().as_ansi(p_decoration='bold')
project_color = colors.get_project_color() context_color = config().context_color().as_ansi(p_decoration='bold')
context_color = colors.get_context_color() link_color = config().link_color().as_ansi(p_decoration='underline')
link_color = colors.get_link_color() metadata_color = config().metadata_color().as_ansi(p_decoration='bold')
metadata_color = colors.get_metadata_color()
todo_a = Todo('(A) Foo')
self.assertEqual(colors.get_priority_color('A'), NEUTRAL_COLOR) todo_b = Todo('(B) Bar')
self.assertEqual(colors.get_priority_color('B'), NEUTRAL_COLOR) todo_c = Todo('(C) FooBar')
self.assertEqual(colors.get_priority_color('C'), NEUTRAL_COLOR)
color_a = config().priority_color(todo_a.priority()).as_ansi()
color_b = config().priority_color(todo_b.priority()).as_ansi()
color_c = config().priority_color(todo_c.priority()).as_ansi()
self.assertEqual(color_a, NEUTRAL_COLOR)
self.assertEqual(color_b, NEUTRAL_COLOR)
self.assertEqual(color_c, NEUTRAL_COLOR)
self.assertEqual(project_color, '') self.assertEqual(project_color, '')
self.assertEqual(context_color, '') self.assertEqual(context_color, '')
self.assertEqual(link_color, '') self.assertEqual(link_color, '')
...@@ -172,19 +179,31 @@ class ColorsTest(TopydoTest): ...@@ -172,19 +179,31 @@ class ColorsTest(TopydoTest):
def test_empty_colorscheme(self): def test_empty_colorscheme(self):
config("test/data/config1") config("test/data/config1")
colors = Colors() project_color = config().project_color().as_ansi(p_decoration='bold')
project_color = colors.get_project_color() context_color = config().context_color().as_ansi(p_decoration='bold')
context_color = colors.get_context_color() link_color = config().link_color().as_ansi(p_decoration='underline')
link_color = colors.get_link_color() metadata_color = config().metadata_color().as_ansi(p_decoration='bold')
metadata_color = colors.get_metadata_color()
todo_a = Todo('(A) Foo')
self.assertEqual(colors.get_priority_color('A'), '\033[0;36m') todo_b = Todo('(B) Bar')
self.assertEqual(colors.get_priority_color('B'), '\033[0;33m') todo_c = Todo('(C) FooBar')
self.assertEqual(colors.get_priority_color('C'), '\033[0;34m')
color_a = config().priority_color(todo_a.priority()).as_ansi()
color_b = config().priority_color(todo_b.priority()).as_ansi()
color_c = config().priority_color(todo_c.priority()).as_ansi()
self.assertEqual(color_a, '\033[0;36m')
self.assertEqual(color_b, '\033[0;33m')
self.assertEqual(color_c, '\033[0;34m')
self.assertEqual(project_color, '\033[1;31m') self.assertEqual(project_color, '\033[1;31m')
self.assertEqual(context_color, '\033[1;35m') self.assertEqual(context_color, '\033[1;35m')
self.assertEqual(link_color, '\033[4;36m') self.assertEqual(link_color, '\033[4;36m')
self.assertEqual(metadata_color, '\033[1;32m') self.assertEqual(metadata_color, '\033[1;32m')
def test_neutral_color(self):
color = Color('NEUTRAL')
self.assertEqual(color.as_ansi(), NEUTRAL_COLOR)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -45,8 +45,7 @@ class ConfigTest(TopydoTest): ...@@ -45,8 +45,7 @@ class ConfigTest(TopydoTest):
""" Bad colour switch value. """ """ Bad colour switch value. """
# boolean settings must first be typecast to integers, because all # boolean settings must first be typecast to integers, because all
# strings evaulate to 'True' # strings evaulate to 'True'
self.assertEqual(config("test/data/ConfigTest4.conf").colors(), self.assertEqual(config("test/data/ConfigTest4.conf").colors(), 16)
bool(int(config().defaults["topydo"]["colors"])))
def test_config06(self): def test_config06(self):
""" Bad auto creation date switch value. """ """ Bad auto creation date switch value. """
...@@ -83,62 +82,76 @@ class ConfigTest(TopydoTest): ...@@ -83,62 +82,76 @@ class ConfigTest(TopydoTest):
self.assertEqual(config("test/data/ConfigTest4.conf").append_parent_contexts(), self.assertEqual(config("test/data/ConfigTest4.conf").append_parent_contexts(),
bool(int(config().defaults["dep"]["append_parent_contexts"]))) bool(int(config().defaults["dep"]["append_parent_contexts"])))
@skip("Error checking not yet implemented")
def test_config14(self): def test_config14(self):
""" Bad priority color value. """ """ Bad priority color value. """
self.assertEqual(config("test/data/ConfigTest4.conf").priority_colors(), self.assertEqual(config("test/data/ConfigTest4.conf").priority_color('A').color, 6)
config().defaults["colorscheme"]["priority_colors"]) self.assertEqual(config("test/data/ConfigTest4.conf").priority_color('B').color, 3)
self.assertEqual(config("test/data/ConfigTest4.conf").priority_color('C').color, 4)
@skip("Error checking not yet implemented")
def test_config15(self): def test_config15(self):
""" Bad project color value. """ """ Bad project color value. """
self.assertEqual(config("test/data/ConfigTest4.conf").project_color(), self.assertTrue(config("test/data/ConfigTest4.conf").project_color().is_neutral())
config().defaults["colorscheme"]["project_color"])
@skip("Error checking not yet implemented")
def test_config16(self): def test_config16(self):
""" Bad context color value. """ """ Bad context color value. """
self.assertEqual(config("test/data/ConfigTest4.conf").context_color(), self.assertTrue(config("test/data/ConfigTest4.conf").context_color().is_neutral())
config().defaults["colorscheme"]["context_color"])
@skip("Error checking not yet implemented")
def test_config17(self): def test_config17(self):
""" Bad metadata color value. """ """ Bad metadata color value. """
self.assertEqual(config("test/data/ConfigTest4.conf").metadata_color(), self.assertTrue(config("test/data/ConfigTest4.conf").metadata_color().is_neutral())
config().defaults["colorscheme"]["metadata_color"])
@skip("Error checking not yet implemented")
def test_config18(self): def test_config18(self):
""" Bad link color value. """ """ Bad link color value. """
self.assertEqual(config("test/data/ConfigTest4.conf").link_color(), self.assertTrue(config("test/data/ConfigTest4.conf").link_color().is_neutral())
config().defaults["colorscheme"]["link_color"])
@skip("Test not yet implemented")
# the test needs to be of the internal function _str_to_dict # the test needs to be of the internal function _str_to_dict
def test_config19(self): def test_config19(self):
""" No priority color value. """ """ No priority color value. """
self.assertEqual(config("test/data/ConfigTest4.conf").priority_colors(), self.assertEqual(config("test/data/ConfigTest4.conf").priority_color('A').color, 6)
config().defaults["colorscheme"]["priority_colors"]) self.assertEqual(config("test/data/ConfigTest4.conf").priority_color('B').color, 3)
self.assertEqual(config("test/data/ConfigTest4.conf").priority_color('C').color, 4)
def test_config20(self): def test_config20(self):
""" No project color value. """ """ No project color value. """
self.assertEqual(config("test/data/ConfigTest5.conf").project_color(), self.assertEqual(config("test/data/ConfigTest5.conf").project_color().color, 1)
config().defaults["colorscheme"]["project_color"])
def test_config21(self): def test_config21(self):
""" No context color value. """ """ No context color value. """
self.assertEqual(config("test/data/ConfigTest5.conf").context_color(), self.assertEqual(config("test/data/ConfigTest5.conf").context_color().color, 5)
config().defaults["colorscheme"]["context_color"])
def test_config22(self): def test_config22(self):
""" No metadata color value. """ """ No metadata color value. """
self.assertEqual(config("test/data/ConfigTest5.conf").metadata_color(), self.assertEqual(config("test/data/ConfigTest5.conf").metadata_color().color, 2)
config().defaults["colorscheme"]["metadata_color"])
def test_config23(self): def test_config23(self):
""" No link color value. """ """ No link color value. """
self.assertEqual(config("test/data/ConfigTest5.conf").link_color(), self.assertEqual(config("test/data/ConfigTest5.conf").link_color().color, 6)
config().defaults["colorscheme"]["link_color"])
def test_config24(self):
""" column_keymap test. """
keymap, keystates = config("test/data/ConfigTest6.conf").column_keymap()
self.assertEqual(keymap['pp'], 'postpone')
self.assertEqual(keymap['ps'], 'postpone_s')
self.assertEqual(keymap['pr'], 'pri')
self.assertEqual(keymap['pra'], 'cmd pri {} a')
self.assertIn('p', keystates)
self.assertIn('g', keystates)
self.assertIn('pp', keystates)
self.assertIn('ps', keystates)
self.assertIn('pr', keystates)
self.assertEqual(keymap['up'], 'up')
self.assertIn('u', keystates)
self.assertEqual(keymap['<Left>'], 'prev_column')
self.assertNotIn('<Lef', keystates)
self.assertEqual(keymap['<Esc>d'], 'delete_column')
self.assertNotIn('<Esc', keystates)
self.assertIn('<Esc>', keystates)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -33,7 +33,7 @@ def _no_prompt(self): ...@@ -33,7 +33,7 @@ def _no_prompt(self):
class DeleteCommandTest(CommandTest): class DeleteCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(DeleteCommandTest, self).setUp() super().setUp()
todos = [ todos = [
"Foo id:1", "Foo id:1",
"Bar p:1", "Bar p:1",
...@@ -48,7 +48,7 @@ class DeleteCommandTest(CommandTest): ...@@ -48,7 +48,7 @@ class DeleteCommandTest(CommandTest):
_no_prompt) _no_prompt)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.todo(1).source(), "Bar") self.assertEqual(self.todolist.todo(1).source(), "Bar")
self.assertEqual(self.output, "| 2| Bar p:1\nRemoved: Foo id:1\n") self.assertEqual(self.output, "| 2| Bar p:1\nRemoved: Foo id:1\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -58,7 +58,7 @@ class DeleteCommandTest(CommandTest): ...@@ -58,7 +58,7 @@ class DeleteCommandTest(CommandTest):
_no_prompt) _no_prompt)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.todo(1).source(), "Bar") self.assertEqual(self.todolist.todo(1).source(), "Bar")
self.assertEqual(self.output, "| 2| Bar p:1\nRemoved: Foo id:1\n") self.assertEqual(self.output, "| 2| Bar p:1\nRemoved: Foo id:1\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -68,7 +68,7 @@ class DeleteCommandTest(CommandTest): ...@@ -68,7 +68,7 @@ class DeleteCommandTest(CommandTest):
_yes_prompt) _yes_prompt)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.count(), 2) self.assertEqual(self.todolist.count(), 2)
self.assertEqual(self.output, self.assertEqual(self.output,
"| 2| Bar p:1\nRemoved: Bar\nRemoved: Foo\n") "| 2| Bar p:1\nRemoved: Bar\nRemoved: Foo\n")
...@@ -79,7 +79,7 @@ class DeleteCommandTest(CommandTest): ...@@ -79,7 +79,7 @@ class DeleteCommandTest(CommandTest):
self.error, _yes_prompt) self.error, _yes_prompt)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.count(), 3) # force won't delete subtasks self.assertEqual(self.todolist.count(), 3) # force won't delete subtasks
self.assertEqual(self.output, "| 2| Bar p:1\nRemoved: Foo id:1\n") self.assertEqual(self.output, "| 2| Bar p:1\nRemoved: Foo id:1\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -89,7 +89,7 @@ class DeleteCommandTest(CommandTest): ...@@ -89,7 +89,7 @@ class DeleteCommandTest(CommandTest):
self.error, _yes_prompt) self.error, _yes_prompt)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.count(), 3) # force won't delete subtasks self.assertEqual(self.todolist.count(), 3) # force won't delete subtasks
self.assertEqual(self.output, "| 2| Bar p:1\nRemoved: Foo id:1\n") self.assertEqual(self.output, "| 2| Bar p:1\nRemoved: Foo id:1\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -98,7 +98,7 @@ class DeleteCommandTest(CommandTest): ...@@ -98,7 +98,7 @@ class DeleteCommandTest(CommandTest):
command = DeleteCommand(["2"], self.todolist, self.out, self.error) command = DeleteCommand(["2"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.todo(1).source(), "Foo") self.assertEqual(self.todolist.todo(1).source(), "Foo")
self.assertEqual(self.output, "Removed: Bar p:1\nThe following todo item(s) became active:\n| 1| Foo\n") self.assertEqual(self.output, "Removed: Bar p:1\nThe following todo item(s) became active:\n| 1| Foo\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -107,7 +107,7 @@ class DeleteCommandTest(CommandTest): ...@@ -107,7 +107,7 @@ class DeleteCommandTest(CommandTest):
command = DeleteCommand(["99"], self.todolist, self.out, self.error) command = DeleteCommand(["99"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
...@@ -115,7 +115,7 @@ class DeleteCommandTest(CommandTest): ...@@ -115,7 +115,7 @@ class DeleteCommandTest(CommandTest):
command = DeleteCommand(["A"], self.todolist, self.out, self.error) command = DeleteCommand(["A"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
...@@ -159,7 +159,7 @@ class DeleteCommandTest(CommandTest): ...@@ -159,7 +159,7 @@ class DeleteCommandTest(CommandTest):
self.error, _yes_prompt) self.error, _yes_prompt)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number given: 99.\n") self.assertEqual(self.errors, "Invalid todo number given: 99.\n")
...@@ -169,7 +169,7 @@ class DeleteCommandTest(CommandTest): ...@@ -169,7 +169,7 @@ class DeleteCommandTest(CommandTest):
self.error, _yes_prompt) self.error, _yes_prompt)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number given: 99.\nInvalid todo number given: A.\n") self.assertEqual(self.errors, "Invalid todo number given: 99.\nInvalid todo number given: A.\n")
...@@ -181,7 +181,7 @@ class DeleteCommandTest(CommandTest): ...@@ -181,7 +181,7 @@ class DeleteCommandTest(CommandTest):
self.out, self.error, None) self.out, self.error, None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, self.assertEqual(self.errors,
u"Invalid todo number given: Fo\u00d3B\u0105r.\n") u"Invalid todo number given: Fo\u00d3B\u0105r.\n")
...@@ -193,7 +193,7 @@ class DeleteCommandTest(CommandTest): ...@@ -193,7 +193,7 @@ class DeleteCommandTest(CommandTest):
result = "Removed: a @test with due:2015-06-03\nRemoved: a @test with +project\n" result = "Removed: a @test with due:2015-06-03\nRemoved: a @test with +project\n"
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.count(), 2) self.assertEqual(self.todolist.count(), 2)
self.assertEqual(self.output, result) self.assertEqual(self.output, result)
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -203,7 +203,7 @@ class DeleteCommandTest(CommandTest): ...@@ -203,7 +203,7 @@ class DeleteCommandTest(CommandTest):
self.todolist, self.out, self.error, None) self.todolist, self.out, self.error, None)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "Removed: a @test with due:2015-06-03\n") self.assertEqual(self.output, "Removed: a @test with due:2015-06-03\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -212,7 +212,7 @@ class DeleteCommandTest(CommandTest): ...@@ -212,7 +212,7 @@ class DeleteCommandTest(CommandTest):
self.todolist, self.out, self.error, None) self.todolist, self.out, self.error, None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
def test_expr_del4(self): def test_expr_del4(self):
""" Remove only relevant todo items. """ """ Remove only relevant todo items. """
...@@ -222,7 +222,7 @@ class DeleteCommandTest(CommandTest): ...@@ -222,7 +222,7 @@ class DeleteCommandTest(CommandTest):
result = "Foo" result = "Foo"
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.count(), 1) self.assertEqual(self.todolist.count(), 1)
self.assertEqual(self.todolist.print_todos(), result) self.assertEqual(self.todolist.print_todos(), result)
...@@ -232,14 +232,14 @@ class DeleteCommandTest(CommandTest): ...@@ -232,14 +232,14 @@ class DeleteCommandTest(CommandTest):
self.error, _yes_prompt) self.error, _yes_prompt)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.count(), 0) self.assertEqual(self.todolist.count(), 0)
def test_empty(self): def test_empty(self):
command = DeleteCommand([], self.todolist, self.out, self.error) command = DeleteCommand([], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, command.usage() + "\n") self.assertEqual(self.errors, command.usage() + "\n")
......
...@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList ...@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList
class DepCommandTest(CommandTest): class DepCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(DepCommandTest, self).setUp() super().setUp()
todos = [ todos = [
"Foo id:1", "Foo id:1",
"Bar p:1", "Bar p:1",
...@@ -40,7 +40,7 @@ class DepCommandTest(CommandTest): ...@@ -40,7 +40,7 @@ class DepCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertTrue(self.todolist.todo(4).has_tag('p', '1')) self.assertTrue(self.todolist.todo(4).has_tag('p', '1'))
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -50,7 +50,7 @@ class DepCommandTest(CommandTest): ...@@ -50,7 +50,7 @@ class DepCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertTrue(self.todolist.todo(4).has_tag('p', '1')) self.assertTrue(self.todolist.todo(4).has_tag('p', '1'))
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -60,7 +60,7 @@ class DepCommandTest(CommandTest): ...@@ -60,7 +60,7 @@ class DepCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
...@@ -69,7 +69,7 @@ class DepCommandTest(CommandTest): ...@@ -69,7 +69,7 @@ class DepCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
...@@ -77,7 +77,7 @@ class DepCommandTest(CommandTest): ...@@ -77,7 +77,7 @@ class DepCommandTest(CommandTest):
command = DepCommand(["add", "1"], self.todolist, self.out, self.error) command = DepCommand(["add", "1"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, command.usage() + "\n") self.assertEqual(self.errors, command.usage() + "\n")
...@@ -86,7 +86,7 @@ class DepCommandTest(CommandTest): ...@@ -86,7 +86,7 @@ class DepCommandTest(CommandTest):
self.out, self.error) self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertTrue(self.todolist.todo(4).has_tag('p', '1')) self.assertTrue(self.todolist.todo(4).has_tag('p', '1'))
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -96,7 +96,7 @@ class DepCommandTest(CommandTest): ...@@ -96,7 +96,7 @@ class DepCommandTest(CommandTest):
self.out, self.error) self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertTrue(self.todolist.todo(1).has_tag('p', '2')) self.assertTrue(self.todolist.todo(1).has_tag('p', '2'))
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -106,7 +106,7 @@ class DepCommandTest(CommandTest): ...@@ -106,7 +106,7 @@ class DepCommandTest(CommandTest):
self.out, self.error) self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertTrue(self.todolist.todo(1).has_tag('p', '2')) self.assertTrue(self.todolist.todo(1).has_tag('p', '2'))
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -116,11 +116,72 @@ class DepCommandTest(CommandTest): ...@@ -116,11 +116,72 @@ class DepCommandTest(CommandTest):
self.out, self.error) self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertTrue(self.todolist.todo(4).has_tag('p', '1')) self.assertTrue(self.todolist.todo(4).has_tag('p', '1'))
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
def add_parentsof_helper(self, p_args):
command = DepCommand(p_args, self.todolist, self.out, self.error)
command.execute()
self.assertTrue(self.todolist.dirty)
self.assertTrue(self.todolist.todo(4).has_tag('p', '1'))
self.assertEqual(self.output, "")
self.assertEqual(self.errors, "")
def test_add10(self):
self.add_parentsof_helper(["add", "4", "parents-of", "2"])
def test_add11(self):
self.add_parentsof_helper(["add", "4", "parent-of", "2"])
def test_add12(self):
self.add_parentsof_helper(["add", "4", "parentsof", "2"])
def test_add13(self):
self.add_parentsof_helper(["add", "4", "parentof", "2"])
def test_add14(self):
command = DepCommand(["add", "4", "parents-of", "5"], self.todolist,
self.out, self.error)
command.execute()
self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "")
self.assertEqual(self.errors, "")
def add_childrenof_helper(self, p_args):
command = DepCommand(p_args, self.todolist, self.out, self.error)
command.execute()
self.assertTrue(self.todolist.dirty)
self.assertTrue(self.todolist.todo(2).has_tag('p', '2'))
self.assertTrue(self.todolist.todo(3).has_tag('p', '2'))
self.assertEqual(self.output, "")
self.assertEqual(self.errors, "")
def test_add15(self):
self.add_childrenof_helper(["add", "4", "children-of", "1"])
def test_add16(self):
self.add_childrenof_helper(["add", "4", "child-of", "1"])
def test_add17(self):
self.add_childrenof_helper(["add", "4", "childrenof", "1"])
def test_add18(self):
self.add_childrenof_helper(["add", "4", "childof", "1"])
def test_add19(self):
command = DepCommand(["add", "4", "children-of", "5"], self.todolist,
self.out, self.error)
command.execute()
self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "")
self.assertEqual(self.errors, "")
def rm_helper(self, p_args): def rm_helper(self, p_args):
""" """
Helper function that checks the removal of the dependency from todo 1 Helper function that checks the removal of the dependency from todo 1
...@@ -129,7 +190,7 @@ class DepCommandTest(CommandTest): ...@@ -129,7 +190,7 @@ class DepCommandTest(CommandTest):
command = DepCommand(p_args, self.todolist, self.out, self.error) command = DepCommand(p_args, self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertTrue(self.todolist.todo(1).has_tag('id', '1')) self.assertTrue(self.todolist.todo(1).has_tag('id', '1'))
self.assertFalse(self.todolist.todo(3).has_tag('p', '1')) self.assertFalse(self.todolist.todo(3).has_tag('p', '1'))
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
...@@ -152,7 +213,7 @@ class DepCommandTest(CommandTest): ...@@ -152,7 +213,7 @@ class DepCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
...@@ -161,7 +222,7 @@ class DepCommandTest(CommandTest): ...@@ -161,7 +222,7 @@ class DepCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
...@@ -169,7 +230,7 @@ class DepCommandTest(CommandTest): ...@@ -169,7 +230,7 @@ class DepCommandTest(CommandTest):
command = DepCommand(["rm", "1"], self.todolist, self.out, self.error) command = DepCommand(["rm", "1"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, command.usage() + "\n") self.assertEqual(self.errors, command.usage() + "\n")
...@@ -178,7 +239,7 @@ class DepCommandTest(CommandTest): ...@@ -178,7 +239,7 @@ class DepCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 2| Bar p:1\n| 3| Baz p:1\n") self.assertEqual(self.output, "| 2| Bar p:1\n| 3| Baz p:1\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -187,7 +248,7 @@ class DepCommandTest(CommandTest): ...@@ -187,7 +248,7 @@ class DepCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
...@@ -196,7 +257,7 @@ class DepCommandTest(CommandTest): ...@@ -196,7 +257,7 @@ class DepCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 1| Foo id:1\n") self.assertEqual(self.output, "| 1| Foo id:1\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -205,7 +266,7 @@ class DepCommandTest(CommandTest): ...@@ -205,7 +266,7 @@ class DepCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
...@@ -213,7 +274,7 @@ class DepCommandTest(CommandTest): ...@@ -213,7 +274,7 @@ class DepCommandTest(CommandTest):
command = DepCommand(["ls", "1"], self.todolist, self.out, self.error) command = DepCommand(["ls", "1"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, command.usage() + "\n") self.assertEqual(self.errors, command.usage() + "\n")
...@@ -221,7 +282,7 @@ class DepCommandTest(CommandTest): ...@@ -221,7 +282,7 @@ class DepCommandTest(CommandTest):
command = DepCommand(["ls"], self.todolist, self.out, self.error) command = DepCommand(["ls"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, command.usage() + "\n") self.assertEqual(self.errors, command.usage() + "\n")
...@@ -230,7 +291,7 @@ class DepCommandTest(CommandTest): ...@@ -230,7 +291,7 @@ class DepCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, command.usage() + "\n") self.assertEqual(self.errors, command.usage() + "\n")
...@@ -239,7 +300,7 @@ class DepCommandTest(CommandTest): ...@@ -239,7 +300,7 @@ class DepCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertFalse(self.errors) self.assertFalse(self.errors)
self.assertFalse(self.todolist.todo(5).has_tag('p', '99')) self.assertFalse(self.todolist.todo(5).has_tag('p', '99'))
...@@ -256,7 +317,7 @@ class DepCommandTest(CommandTest): ...@@ -256,7 +317,7 @@ class DepCommandTest(CommandTest):
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, command.usage() + "\n") self.assertEqual(self.errors, command.usage() + "\n")
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
def test_no_subsubcommand(self): def test_no_subsubcommand(self):
command = DepCommand([], self.todolist, self.out, self.error) command = DepCommand([], self.todolist, self.out, self.error)
...@@ -264,7 +325,7 @@ class DepCommandTest(CommandTest): ...@@ -264,7 +325,7 @@ class DepCommandTest(CommandTest):
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, command.usage() + "\n") self.assertEqual(self.errors, command.usage() + "\n")
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
def test_help(self): def test_help(self):
command = DepCommand(["help"], self.todolist, self.out, self.error) command = DepCommand(["help"], self.todolist, self.out, self.error)
......
...@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList ...@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList
class DepriCommandTest(CommandTest): class DepriCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(DepriCommandTest, self).setUp() super().setUp()
todos = [ todos = [
"(A) Foo", "(A) Foo",
"Bar", "Bar",
...@@ -39,7 +39,7 @@ class DepriCommandTest(CommandTest): ...@@ -39,7 +39,7 @@ class DepriCommandTest(CommandTest):
command = DepriCommand(["1"], self.todolist, self.out, self.error) command = DepriCommand(["1"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.todo(1).priority(), None) self.assertEqual(self.todolist.todo(1).priority(), None)
self.assertEqual(self.output, "Priority removed.\n| 1| Foo\n") self.assertEqual(self.output, "Priority removed.\n| 1| Foo\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -48,7 +48,7 @@ class DepriCommandTest(CommandTest): ...@@ -48,7 +48,7 @@ class DepriCommandTest(CommandTest):
command = DepriCommand(["2"], self.todolist, self.out, self.error) command = DepriCommand(["2"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.todolist.todo(2).priority(), None) self.assertEqual(self.todolist.todo(2).priority(), None)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -57,7 +57,7 @@ class DepriCommandTest(CommandTest): ...@@ -57,7 +57,7 @@ class DepriCommandTest(CommandTest):
command = DepriCommand(["Foo"], self.todolist, self.out, self.error) command = DepriCommand(["Foo"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.todo(1).priority(), None) self.assertEqual(self.todolist.todo(1).priority(), None)
self.assertEqual(self.output, "Priority removed.\n| 1| Foo\n") self.assertEqual(self.output, "Priority removed.\n| 1| Foo\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -67,7 +67,7 @@ class DepriCommandTest(CommandTest): ...@@ -67,7 +67,7 @@ class DepriCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.todo(1).priority(), None) self.assertEqual(self.todolist.todo(1).priority(), None)
self.assertEqual(self.todolist.todo(3).priority(), None) self.assertEqual(self.todolist.todo(3).priority(), None)
self.assertEqual(self.output, "Priority removed.\n| 1| Foo\nPriority removed.\n| 3| Baz\n") self.assertEqual(self.output, "Priority removed.\n| 1| Foo\nPriority removed.\n| 3| Baz\n")
...@@ -80,7 +80,7 @@ class DepriCommandTest(CommandTest): ...@@ -80,7 +80,7 @@ class DepriCommandTest(CommandTest):
result = "Priority removed.\n| 4| a @test with due:2015-06-03\nPriority removed.\n| 5| a @test with +project p:1\n" result = "Priority removed.\n| 4| a @test with due:2015-06-03\nPriority removed.\n| 5| a @test with +project p:1\n"
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, result) self.assertEqual(self.output, result)
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -91,7 +91,7 @@ class DepriCommandTest(CommandTest): ...@@ -91,7 +91,7 @@ class DepriCommandTest(CommandTest):
result = "Priority removed.\n| 4| a @test with due:2015-06-03\n" result = "Priority removed.\n| 4| a @test with due:2015-06-03\n"
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, result) self.assertEqual(self.output, result)
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -100,7 +100,7 @@ class DepriCommandTest(CommandTest): ...@@ -100,7 +100,7 @@ class DepriCommandTest(CommandTest):
self.todolist, self.out, self.error, None) self.todolist, self.out, self.error, None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
def test_expr_depri4(self): def test_expr_depri4(self):
""" Don't remove priority from unrelevant todo items. """ """ Don't remove priority from unrelevant todo items. """
...@@ -108,7 +108,7 @@ class DepriCommandTest(CommandTest): ...@@ -108,7 +108,7 @@ class DepriCommandTest(CommandTest):
self.error, None) self.error, None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
def test_expr_depri5(self): def test_expr_depri5(self):
""" Force unprioritizing unrelevant items with additional -x flag. """ """ Force unprioritizing unrelevant items with additional -x flag. """
...@@ -116,7 +116,7 @@ class DepriCommandTest(CommandTest): ...@@ -116,7 +116,7 @@ class DepriCommandTest(CommandTest):
self.error, None) self.error, None)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "Priority removed.\n| 6| Bax id:1\n") self.assertEqual(self.output, "Priority removed.\n| 6| Bax id:1\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -124,7 +124,7 @@ class DepriCommandTest(CommandTest): ...@@ -124,7 +124,7 @@ class DepriCommandTest(CommandTest):
command = DepriCommand(["99"], self.todolist, self.out, self.error) command = DepriCommand(["99"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
...@@ -133,7 +133,7 @@ class DepriCommandTest(CommandTest): ...@@ -133,7 +133,7 @@ class DepriCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, "Invalid todo number given: 99.\n") self.assertEqual(self.errors, "Invalid todo number given: 99.\n")
...@@ -142,7 +142,7 @@ class DepriCommandTest(CommandTest): ...@@ -142,7 +142,7 @@ class DepriCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, "Invalid todo number given: 99.\nInvalid todo number given: FooBar.\n") self.assertEqual(self.errors, "Invalid todo number given: 99.\nInvalid todo number given: FooBar.\n")
...@@ -154,7 +154,7 @@ class DepriCommandTest(CommandTest): ...@@ -154,7 +154,7 @@ class DepriCommandTest(CommandTest):
self.out, self.error, None) self.out, self.error, None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, self.assertEqual(self.errors,
u"Invalid todo number given: Fo\u00d3B\u0105r.\n") u"Invalid todo number given: Fo\u00d3B\u0105r.\n")
...@@ -163,7 +163,7 @@ class DepriCommandTest(CommandTest): ...@@ -163,7 +163,7 @@ class DepriCommandTest(CommandTest):
command = DepriCommand([], self.todolist, self.out, self.error) command = DepriCommand([], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, command.usage() + "\n") self.assertEqual(self.errors, command.usage() + "\n")
......
...@@ -32,7 +32,7 @@ def _no_prompt(self): ...@@ -32,7 +32,7 @@ def _no_prompt(self):
class DoCommandTest(CommandTest): class DoCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(DoCommandTest, self).setUp() super().setUp()
todos = [ todos = [
"Foo id:1", "Foo id:1",
"Bar p:1", "Bar p:1",
...@@ -61,7 +61,7 @@ class DoCommandTest(CommandTest): ...@@ -61,7 +61,7 @@ class DoCommandTest(CommandTest):
_no_prompt) _no_prompt)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertTrue(self.todolist.todo(3).is_completed()) self.assertTrue(self.todolist.todo(3).is_completed())
self.assertEqual(self.output, self.assertEqual(self.output,
"Completed: x {} Baz p:1\n".format(self.today)) "Completed: x {} Baz p:1\n".format(self.today))
...@@ -77,7 +77,7 @@ class DoCommandTest(CommandTest): ...@@ -77,7 +77,7 @@ class DoCommandTest(CommandTest):
for number in [1, 2, 3]: for number in [1, 2, 3]:
self.assertTrue(self.todolist.todo(number).is_completed()) self.assertTrue(self.todolist.todo(number).is_completed())
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertFalse(self.todolist.todo(4).is_completed()) self.assertFalse(self.todolist.todo(4).is_completed())
self.assertEqual(self.output, result) self.assertEqual(self.output, result)
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -89,7 +89,7 @@ class DoCommandTest(CommandTest): ...@@ -89,7 +89,7 @@ class DoCommandTest(CommandTest):
result = "| 2| Bar p:1\n| 3| Baz p:1\nCompleted: x {} Foo id:1\n".format(self.today) result = "| 2| Bar p:1\n| 3| Baz p:1\nCompleted: x {} Foo id:1\n".format(self.today)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertTrue(self.todolist.todo(1).is_completed()) self.assertTrue(self.todolist.todo(1).is_completed())
self.assertFalse(self.todolist.todo(2).is_completed()) self.assertFalse(self.todolist.todo(2).is_completed())
self.assertFalse(self.todolist.todo(3).is_completed()) self.assertFalse(self.todolist.todo(3).is_completed())
...@@ -130,7 +130,7 @@ class DoCommandTest(CommandTest): ...@@ -130,7 +130,7 @@ class DoCommandTest(CommandTest):
command = DoCommand(p_flags, self.todolist, self.out, self.error) command = DoCommand(p_flags, self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
self.assertEqual(self.todolist.count(), 12) self.assertEqual(self.todolist.count(), 12)
...@@ -162,7 +162,7 @@ class DoCommandTest(CommandTest): ...@@ -162,7 +162,7 @@ class DoCommandTest(CommandTest):
command = DoCommand(["99"], self.todolist, self.out, self.error) command = DoCommand(["99"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
...@@ -170,7 +170,7 @@ class DoCommandTest(CommandTest): ...@@ -170,7 +170,7 @@ class DoCommandTest(CommandTest):
command = DoCommand(["AAA"], self.todolist, self.out, self.error) command = DoCommand(["AAA"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
...@@ -179,7 +179,7 @@ class DoCommandTest(CommandTest): ...@@ -179,7 +179,7 @@ class DoCommandTest(CommandTest):
_yes_prompt) _yes_prompt)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
...@@ -209,7 +209,7 @@ class DoCommandTest(CommandTest): ...@@ -209,7 +209,7 @@ class DoCommandTest(CommandTest):
command = DoCommand(["5"], self.todolist, self.out, self.error) command = DoCommand(["5"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.todolist.todo(5).completion_date(), self.assertEqual(self.todolist.todo(5).completion_date(),
date(2014, 10, 18)) date(2014, 10, 18))
self.assertFalse(self.output) self.assertFalse(self.output)
...@@ -219,7 +219,7 @@ class DoCommandTest(CommandTest): ...@@ -219,7 +219,7 @@ class DoCommandTest(CommandTest):
command = DoCommand(["baz"], self.todolist, self.out, self.error) command = DoCommand(["baz"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertTrue(self.todolist.todo(3).is_completed()) self.assertTrue(self.todolist.todo(3).is_completed())
self.assertEqual(self.output, self.assertEqual(self.output,
"Completed: x {} Baz p:1\n".format(self.today)) "Completed: x {} Baz p:1\n".format(self.today))
...@@ -230,7 +230,7 @@ class DoCommandTest(CommandTest): ...@@ -230,7 +230,7 @@ class DoCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "Completed: x 2014-11-18 Baz p:1\n") self.assertEqual(self.output, "Completed: x 2014-11-18 Baz p:1\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -239,7 +239,7 @@ class DoCommandTest(CommandTest): ...@@ -239,7 +239,7 @@ class DoCommandTest(CommandTest):
self.error, _yes_prompt) self.error, _yes_prompt)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "| 2| Bar p:1\n| 3| Baz p:1\nCompleted: x 2014-11-18 Bar p:1\nCompleted: x 2014-11-18 Baz p:1\nCompleted: x 2014-11-18 Foo id:1\n") self.assertEqual(self.output, "| 2| Bar p:1\n| 3| Baz p:1\nCompleted: x 2014-11-18 Bar p:1\nCompleted: x 2014-11-18 Baz p:1\nCompleted: x 2014-11-18 Foo id:1\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -248,7 +248,7 @@ class DoCommandTest(CommandTest): ...@@ -248,7 +248,7 @@ class DoCommandTest(CommandTest):
self.out, self.error) self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "Completed: x 2014-11-18 Baz p:1\n") self.assertEqual(self.output, "Completed: x 2014-11-18 Baz p:1\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -257,7 +257,7 @@ class DoCommandTest(CommandTest): ...@@ -257,7 +257,7 @@ class DoCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, self.assertEqual(self.output,
"Completed: x {} Baz p:1\n".format(self.today)) "Completed: x {} Baz p:1\n".format(self.today))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -271,7 +271,7 @@ class DoCommandTest(CommandTest): ...@@ -271,7 +271,7 @@ class DoCommandTest(CommandTest):
self.out, self.error) self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "| 12| {today} Recurring! rec:1d due:{today}\nCompleted: x {yesterday} Recurring! rec:1d\n".format(today=self.today, yesterday=self.yesterday)) self.assertEqual(self.output, "| 12| {today} Recurring! rec:1d due:{today}\nCompleted: x {yesterday} Recurring! rec:1d\n".format(today=self.today, yesterday=self.yesterday))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -285,7 +285,7 @@ class DoCommandTest(CommandTest): ...@@ -285,7 +285,7 @@ class DoCommandTest(CommandTest):
self.out, self.error) self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "| 12| {today} Recurring! rec:1d due:{today}\nCompleted: x {yesterday} Recurring! rec:1d\n".format(today=self.today, yesterday=self.yesterday)) self.assertEqual(self.output, "| 12| {today} Recurring! rec:1d due:{today}\nCompleted: x {yesterday} Recurring! rec:1d\n".format(today=self.today, yesterday=self.yesterday))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -298,7 +298,7 @@ class DoCommandTest(CommandTest): ...@@ -298,7 +298,7 @@ class DoCommandTest(CommandTest):
self.out, self.error) self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "| 12| {today} Strict due:2014-01-02 rec:1d\nCompleted: x {yesterday} Strict due:2014-01-01 rec:1d\n".format(today=self.today, yesterday=self.yesterday)) self.assertEqual(self.output, "| 12| {today} Strict due:2014-01-02 rec:1d\nCompleted: x {yesterday} Strict due:2014-01-01 rec:1d\n".format(today=self.today, yesterday=self.yesterday))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -310,7 +310,7 @@ class DoCommandTest(CommandTest): ...@@ -310,7 +310,7 @@ class DoCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, self.assertEqual(self.output,
"Completed: x {} Baz p:1\n".format(self.yesterday)) "Completed: x {} Baz p:1\n".format(self.yesterday))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -323,7 +323,7 @@ class DoCommandTest(CommandTest): ...@@ -323,7 +323,7 @@ class DoCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, self.assertEqual(self.output,
"Completed: x {} Baz p:1\n".format(self.yesterday)) "Completed: x {} Baz p:1\n".format(self.yesterday))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -385,7 +385,7 @@ class DoCommandTest(CommandTest): ...@@ -385,7 +385,7 @@ class DoCommandTest(CommandTest):
self.out, self.error, None) self.out, self.error, None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.errors, self.assertEqual(self.errors,
u"Invalid todo number given: Fo\u00d3B\u0105r.\n") u"Invalid todo number given: Fo\u00d3B\u0105r.\n")
...@@ -394,7 +394,7 @@ class DoCommandTest(CommandTest): ...@@ -394,7 +394,7 @@ class DoCommandTest(CommandTest):
self.error, None) self.error, None)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "Completed: x {t} a @test with due:2015-06-03\nCompleted: x {t} a @test with +project\n".format(t=self.today)) self.assertEqual(self.output, "Completed: x {t} a @test with due:2015-06-03\nCompleted: x {t} a @test with +project\n".format(t=self.today))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -403,7 +403,7 @@ class DoCommandTest(CommandTest): ...@@ -403,7 +403,7 @@ class DoCommandTest(CommandTest):
self.out, self.error, None) self.out, self.error, None)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "Completed: x {} a @test with due:2015-06-03\n".format(self.today)) self.assertEqual(self.output, "Completed: x {} a @test with due:2015-06-03\n".format(self.today))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -412,7 +412,7 @@ class DoCommandTest(CommandTest): ...@@ -412,7 +412,7 @@ class DoCommandTest(CommandTest):
self.todolist, self.out, self.error, None) self.todolist, self.out, self.error, None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
def test_expr_do4(self): def test_expr_do4(self):
""" Don't do anything with unrelevant todo items. """ """ Don't do anything with unrelevant todo items. """
...@@ -420,7 +420,7 @@ class DoCommandTest(CommandTest): ...@@ -420,7 +420,7 @@ class DoCommandTest(CommandTest):
None) None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
def test_expr_do5(self): def test_expr_do5(self):
""" Force marking unrelevant items as done with additional -x flag. """ """ Force marking unrelevant items as done with additional -x flag. """
...@@ -430,7 +430,7 @@ class DoCommandTest(CommandTest): ...@@ -430,7 +430,7 @@ class DoCommandTest(CommandTest):
result = "| 2| Bar p:1\n| 3| Baz p:1\nCompleted: x {t} Bar p:1\nCompleted: x {t} Baz p:1\nCompleted: x {t} Foo id:1\n".format(t=self.today) result = "| 2| Bar p:1\n| 3| Baz p:1\nCompleted: x {t} Bar p:1\nCompleted: x {t} Baz p:1\nCompleted: x {t} Foo id:1\n".format(t=self.today)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, result) self.assertEqual(self.output, result)
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -450,7 +450,7 @@ class DoCommandTest(CommandTest): ...@@ -450,7 +450,7 @@ class DoCommandTest(CommandTest):
command = DoCommand([], self.todolist, self.out, self.error) command = DoCommand([], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, command.usage() + "\n") self.assertEqual(self.errors, command.usage() + "\n")
......
...@@ -33,7 +33,7 @@ except ImportError: ...@@ -33,7 +33,7 @@ except ImportError:
class EditCommandTest(CommandTest): class EditCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(EditCommandTest, self).setUp() super().setUp()
todos = [ todos = [
"Foo id:1", "Foo id:1",
"Bar p:1 @test", "Bar p:1 @test",
...@@ -56,7 +56,7 @@ class EditCommandTest(CommandTest): ...@@ -56,7 +56,7 @@ class EditCommandTest(CommandTest):
command.execute() command.execute()
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.print_todos(), u"Bar p:1 @test\nBaz @test\nFo\u00f3B\u0105\u017a\nFoo id:1") self.assertEqual(self.todolist.print_todos(), u"Bar p:1 @test\nBaz @test\nFo\u00f3B\u0105\u017a\nFoo id:1")
@mock.patch('topydo.commands.EditCommand._is_edited') @mock.patch('topydo.commands.EditCommand._is_edited')
...@@ -72,7 +72,7 @@ class EditCommandTest(CommandTest): ...@@ -72,7 +72,7 @@ class EditCommandTest(CommandTest):
None) None)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
self.assertEqual(self.todolist.print_todos(), u"Foo id:1\nBaz @test\nFo\u00f3B\u0105\u017a\nLazy Cat") self.assertEqual(self.todolist.print_todos(), u"Foo id:1\nBaz @test\nFo\u00f3B\u0105\u017a\nLazy Cat")
...@@ -82,7 +82,7 @@ class EditCommandTest(CommandTest): ...@@ -82,7 +82,7 @@ class EditCommandTest(CommandTest):
None) None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
def test_edit04(self): def test_edit04(self):
...@@ -91,7 +91,7 @@ class EditCommandTest(CommandTest): ...@@ -91,7 +91,7 @@ class EditCommandTest(CommandTest):
self.error, None) self.error, None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.errors, "Invalid todo number given: 5.\n") self.assertEqual(self.errors, "Invalid todo number given: 5.\n")
def test_edit05(self): def test_edit05(self):
...@@ -102,7 +102,7 @@ class EditCommandTest(CommandTest): ...@@ -102,7 +102,7 @@ class EditCommandTest(CommandTest):
self.out, self.error, None) self.out, self.error, None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.errors, self.assertEqual(self.errors,
u"Invalid todo number given: Fo\u00d3B\u0105r.\n") u"Invalid todo number given: Fo\u00d3B\u0105r.\n")
...@@ -119,7 +119,7 @@ class EditCommandTest(CommandTest): ...@@ -119,7 +119,7 @@ class EditCommandTest(CommandTest):
self.out, self.error, None) self.out, self.error, None)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
self.assertEqual(self.todolist.print_todos(), self.assertEqual(self.todolist.print_todos(),
u"Foo id:1\nBar p:1 @test\nBaz @test\nLazy Cat") u"Foo id:1\nBar p:1 @test\nBaz @test\nLazy Cat")
...@@ -137,7 +137,7 @@ class EditCommandTest(CommandTest): ...@@ -137,7 +137,7 @@ class EditCommandTest(CommandTest):
self.error, None) self.error, None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.errors, "Editing aborted. Nothing to do.\n") self.assertEqual(self.errors, "Editing aborted. Nothing to do.\n")
self.assertEqual(self.todolist.print_todos(), u"Foo id:1\nBar p:1 @test\nBaz @test\nFo\u00f3B\u0105\u017a") self.assertEqual(self.todolist.print_todos(), u"Foo id:1\nBar p:1 @test\nBaz @test\nFo\u00f3B\u0105\u017a")
...@@ -158,7 +158,7 @@ class EditCommandTest(CommandTest): ...@@ -158,7 +158,7 @@ class EditCommandTest(CommandTest):
expected = u"| 3| Lazy Cat\n| 4| Lazy Dog\n" expected = u"| 3| Lazy Cat\n| 4| Lazy Dog\n"
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, expected) self.assertEqual(self.output, expected)
self.assertEqual(self.todolist.print_todos(), u"Foo id:1\nFo\u00f3B\u0105\u017a\nLazy Cat\nLazy Dog") self.assertEqual(self.todolist.print_todos(), u"Foo id:1\nFo\u00f3B\u0105\u017a\nLazy Cat\nLazy Dog")
......
...@@ -301,7 +301,7 @@ class FilterTest(TopydoTest): ...@@ -301,7 +301,7 @@ class FilterTest(TopydoTest):
class OrdinalTagFilterTest(TopydoTest): class OrdinalTagFilterTest(TopydoTest):
def setUp(self): def setUp(self):
super(OrdinalTagFilterTest, self).setUp() super().setUp()
today = date.today() today = date.today()
tomorrow = today + timedelta(1) tomorrow = today + timedelta(1)
...@@ -379,9 +379,87 @@ class OrdinalTagFilterTest(TopydoTest): ...@@ -379,9 +379,87 @@ class OrdinalTagFilterTest(TopydoTest):
self.assertEqual(result[0].source(), self.todo3) self.assertEqual(result[0].source(), self.todo3)
class CreationFilterTest(TopydoTest):
def setUp(self):
super().setUp()
self.todo1 = "2015-12-19 With creation date."
self.todo2 = "Without creation date."
self.todos = [Todo(self.todo1), Todo(self.todo2)]
def test_filter1(self):
cf = Filter.CreationFilter('create:2015-12-19')
result = cf.filter(self.todos)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].source(), self.todo1)
def test_filter2(self):
cf = Filter.CreationFilter('creation:2015-12-19')
result = cf.filter(self.todos)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].source(), self.todo1)
def test_filter3(self):
cf = Filter.CreationFilter('created:2015-12-19')
result = cf.filter(self.todos)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].source(), self.todo1)
class CompletionFilterTest(TopydoTest):
def setUp(self):
super().setUp()
self.todo1 = "2015-12-19 With creation date."
self.todo2 = "x 2015-12-19 2015-12-18 Without creation date."
self.todo3 = "x 2015-12-18 Without creation date."
self.todos = [Todo(self.todo1), Todo(self.todo2), Todo(self.todo3)]
def test_filter1(self):
cf = Filter.CompletionFilter('complete:2015-12-19')
result = cf.filter(self.todos)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].source(), self.todo2)
def test_filter2(self):
cf = Filter.CompletionFilter('completed:2015-12-19')
result = cf.filter(self.todos)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].source(), self.todo2)
def test_filter3(self):
cf = Filter.CompletionFilter('completion:2015-12-19')
result = cf.filter(self.todos)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].source(), self.todo2)
def test_filter4(self):
cf = Filter.CompletionFilter('completion:<=2015-12-19')
result = cf.filter(self.todos)
self.assertEqual(len(result), 2)
self.assertEqual(result[0].source(), self.todo2)
self.assertEqual(result[1].source(), self.todo3)
class PriorityFilterTest(TopydoTest): class PriorityFilterTest(TopydoTest):
def setUp(self): def setUp(self):
super(PriorityFilterTest, self).setUp() super().setUp()
self.todo1 = "(A) Foo" self.todo1 = "(A) Foo"
self.todo2 = "(B) Bar" self.todo2 = "(B) Bar"
......
...@@ -21,8 +21,8 @@ from topydo.Commands import get_subcommand ...@@ -21,8 +21,8 @@ from topydo.Commands import get_subcommand
from topydo.commands.AddCommand import AddCommand from topydo.commands.AddCommand import AddCommand
from topydo.commands.DeleteCommand import DeleteCommand from topydo.commands.DeleteCommand import DeleteCommand
from topydo.commands.ListCommand import ListCommand 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): class GetSubcommandTest(TopydoTest):
def test_normal_cmd(self): def test_normal_cmd(self):
...@@ -60,6 +60,14 @@ class GetSubcommandTest(TopydoTest): ...@@ -60,6 +60,14 @@ class GetSubcommandTest(TopydoTest):
self.assertTrue(issubclass(real_cmd, ListCommand)) self.assertTrue(issubclass(real_cmd, ListCommand))
self.assertEqual(final_args, [u"\u263b"]) self.assertEqual(final_args, [u"\u263b"])
def test_alias04(self):
config("test/data/aliases.conf")
args = ["star", "foo"]
real_cmd, final_args = get_subcommand(args)
self.assertTrue(issubclass(real_cmd, TagCommand))
self.assertEqual(final_args, ["foo", "star", "1"])
def test_default_cmd01(self): def test_default_cmd01(self):
args = ["bar"] args = ["bar"]
real_cmd, final_args = get_subcommand(args) real_cmd, final_args = get_subcommand(args)
...@@ -111,6 +119,15 @@ class GetSubcommandTest(TopydoTest): ...@@ -111,6 +119,15 @@ class GetSubcommandTest(TopydoTest):
real_cmd, final_args = get_subcommand(args) real_cmd, final_args = get_subcommand(args)
self.assertEqual(real_cmd, None) 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): def test_help(self):
real_cmd, final_args = get_subcommand(['help', 'nonexisting']) real_cmd, final_args = get_subcommand(['help', 'nonexisting'])
self.assertFalse(real_cmd) self.assertFalse(real_cmd)
......
...@@ -22,7 +22,7 @@ from topydo.lib.Graph import DirectedGraph ...@@ -22,7 +22,7 @@ from topydo.lib.Graph import DirectedGraph
class GraphTest(TopydoTest): class GraphTest(TopydoTest):
def setUp(self): def setUp(self):
super(GraphTest, self).setUp() super().setUp()
self.graph = DirectedGraph() self.graph = DirectedGraph()
......
...@@ -17,23 +17,33 @@ ...@@ -17,23 +17,33 @@
import codecs import codecs
import re import re
import unittest import unittest
from collections import namedtuple
from test.command_testcase import CommandTest from test.command_testcase import CommandTest
from test.facilities import load_file_to_todolist from test.facilities import load_file_to_todolist
from topydo.commands.ListCommand import ListCommand from topydo.commands.ListCommand import ListCommand
from topydo.lib.Config import config from topydo.lib.Config import config
# We're searching for 'mock'
# 'mock' was added as 'unittest.mock' in Python 3.3, but PyPy 3 is based on Python 3.2
# pylint: disable=no-name-in-module
try:
from unittest import mock
except ImportError:
import mock
class ListCommandTest(CommandTest): class ListCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(ListCommandTest, self).setUp() super().setUp()
self.todolist = load_file_to_todolist("test/data/ListCommandTest.txt") self.todolist = load_file_to_todolist("test/data/ListCommandTest.txt")
self.terminal_size = namedtuple('terminal_size', ['columns', 'lines'])
def test_list01(self): def test_list01(self):
command = ListCommand([""], self.todolist, self.out, self.error) command = ListCommand([""], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n") self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -42,7 +52,7 @@ class ListCommandTest(CommandTest): ...@@ -42,7 +52,7 @@ class ListCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 2| (D) Bar @Context1 +Project2\n") self.assertEqual(self.output, "| 2| (D) Bar @Context1 +Project2\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -51,7 +61,7 @@ class ListCommandTest(CommandTest): ...@@ -51,7 +61,7 @@ class ListCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 3| (C) Baz @Context1 +Project1 key:value\n| 2| (D) Bar @Context1 +Project2\n") self.assertEqual(self.output, "| 3| (C) Baz @Context1 +Project1 key:value\n| 2| (D) Bar @Context1 +Project2\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -59,7 +69,7 @@ class ListCommandTest(CommandTest): ...@@ -59,7 +69,7 @@ class ListCommandTest(CommandTest):
command = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n| 3| (C) Baz @Context1 +Project1 key:value\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n| 6| x 2014-12-12 Completed but with date:2014-12-12\n") self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n| 3| (C) Baz @Context1 +Project1 key:value\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n| 6| x 2014-12-12 Completed but with date:2014-12-12\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -68,7 +78,7 @@ class ListCommandTest(CommandTest): ...@@ -68,7 +78,7 @@ class ListCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -77,7 +87,7 @@ class ListCommandTest(CommandTest): ...@@ -77,7 +87,7 @@ class ListCommandTest(CommandTest):
self.out, self.error) self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 3| (C) Baz @Context1 +Project1 key:value\n| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n") self.assertEqual(self.output, "| 3| (C) Baz @Context1 +Project1 key:value\n| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -86,7 +96,7 @@ class ListCommandTest(CommandTest): ...@@ -86,7 +96,7 @@ class ListCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n") self.assertEqual(self.output, "| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -95,7 +105,7 @@ class ListCommandTest(CommandTest): ...@@ -95,7 +105,7 @@ class ListCommandTest(CommandTest):
self.out, self.error) self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n") self.assertEqual(self.output, "| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -104,7 +114,7 @@ class ListCommandTest(CommandTest): ...@@ -104,7 +114,7 @@ class ListCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 2| (D) Bar @Context1 +Project2\n") self.assertEqual(self.output, "| 2| (D) Bar @Context1 +Project2\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -114,7 +124,7 @@ class ListCommandTest(CommandTest): ...@@ -114,7 +124,7 @@ class ListCommandTest(CommandTest):
command = ListCommand(["project"], self.todolist, self.out, self.error) command = ListCommand(["project"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n") self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -125,7 +135,7 @@ class ListCommandTest(CommandTest): ...@@ -125,7 +135,7 @@ class ListCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n| 3| (C) Baz @Context1 +Project1 key:value\n| 2| (D) Bar @Context1 +Project2\n") self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n| 3| (C) Baz @Context1 +Project1 key:value\n| 2| (D) Bar @Context1 +Project2\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -134,7 +144,7 @@ class ListCommandTest(CommandTest): ...@@ -134,7 +144,7 @@ class ListCommandTest(CommandTest):
self.todolist, self.out, self.error) self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n| 3| (C) Baz @Context1 +Project1 key:value\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 6| x 2014-12-12 Completed but with date:2014-12-12\n") self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n| 3| (C) Baz @Context1 +Project1 key:value\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 6| x 2014-12-12 Completed but with date:2014-12-12\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -144,7 +154,7 @@ class ListCommandTest(CommandTest): ...@@ -144,7 +154,7 @@ class ListCommandTest(CommandTest):
command = ListCommand([], self.todolist, self.out, self.error) command = ListCommand([], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, " | 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n | 4| (C) Drink beer @ home\n | 5| (C) 13 + 29 = 42\n | 2| (D) Bar @Context1 +Project2\n") self.assertEqual(self.output, " | 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n | 4| (C) Drink beer @ home\n | 5| (C) 13 + 29 = 42\n | 2| (D) Bar @Context1 +Project2\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -152,7 +162,7 @@ class ListCommandTest(CommandTest): ...@@ -152,7 +162,7 @@ class ListCommandTest(CommandTest):
command = ListCommand(["p:<10"], self.todolist, self.out, self.error) command = ListCommand(["p:<10"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 2| (D) Bar @Context1 +Project2\n") self.assertEqual(self.output, "| 2| (D) Bar @Context1 +Project2\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -162,7 +172,7 @@ class ListCommandTest(CommandTest): ...@@ -162,7 +172,7 @@ class ListCommandTest(CommandTest):
command = ListCommand([], self.todolist, self.out, self.error) command = ListCommand([], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "|t5c| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n|wa5| (C) Drink beer @ home\n|z63| (C) 13 + 29 = 42\n|mfg| (D) Bar @Context1 +Project2\n") self.assertEqual(self.output, "|t5c| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n|wa5| (C) Drink beer @ home\n|z63| (C) 13 + 29 = 42\n|mfg| (D) Bar @Context1 +Project2\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -171,7 +181,7 @@ class ListCommandTest(CommandTest): ...@@ -171,7 +181,7 @@ class ListCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, self.assertEqual(self.output,
"| 3| (C) Baz @Context1 +Project1 key:value\n") "| 3| (C) Baz @Context1 +Project1 key:value\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -181,7 +191,7 @@ class ListCommandTest(CommandTest): ...@@ -181,7 +191,7 @@ class ListCommandTest(CommandTest):
self.out, self.error) self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 6| x 2014-12-12 Completed but with date:2014-12-12\n") self.assertEqual(self.output, "| 6| x 2014-12-12 Completed but with date:2014-12-12\n")
def test_list19(self): def test_list19(self):
...@@ -192,7 +202,7 @@ class ListCommandTest(CommandTest): ...@@ -192,7 +202,7 @@ class ListCommandTest(CommandTest):
self.out, self.error) self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 3| (C) Baz @Context1 +Project1 id:1 key:value\n| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n") self.assertEqual(self.output, "| 3| (C) Baz @Context1 +Project1 id:1 key:value\n| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -200,7 +210,7 @@ class ListCommandTest(CommandTest): ...@@ -200,7 +210,7 @@ class ListCommandTest(CommandTest):
command = ListCommand(["-f text"], self.todolist, self.out, self.error) command = ListCommand(["-f text"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n") self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -209,7 +219,7 @@ class ListCommandTest(CommandTest): ...@@ -209,7 +219,7 @@ class ListCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n") self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -221,7 +231,7 @@ class ListCommandTest(CommandTest): ...@@ -221,7 +231,7 @@ class ListCommandTest(CommandTest):
command = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, '| 1| Foo.\n') self.assertEqual(self.output, '| 1| Foo.\n')
def test_list31(self): def test_list31(self):
...@@ -313,8 +323,64 @@ class ListCommandTest(CommandTest): ...@@ -313,8 +323,64 @@ class ListCommandTest(CommandTest):
self.assertEqual(self.errors, "option -z not recognized\n") self.assertEqual(self.errors, "option -z not recognized\n")
def test_list42(self): def test_list42(self):
command = ListCommand(["-x", "+Project1", "-id:1"], self.todolist, self.out, command = ListCommand(["-x", "+Project1", "-id:1"], self.todolist,
self.error) self.out, self.error)
command.execute()
self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n")
self.assertEqual(self.errors, "")
def test_list43(self):
"""Test basic 'N' parameter."""
command = ListCommand(["-N"], self.todolist, self.out, self.error)
command.execute()
self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n")
self.assertEqual(self.errors, "")
@mock.patch('topydo.commands.ListCommand.get_terminal_size')
def test_list44(self, mock_terminal_size):
"""
Test 'N' parameter with output longer than available terminal lines.
"""
self.todolist = load_file_to_todolist("test/data/ListCommand_50_items.txt")
mock_terminal_size.return_value = self.terminal_size(80, 23)
command = ListCommand(["-N"], self.todolist, self.out, self.error)
command.execute()
self.assertEqual(self.output, "| 1| (A) item 1\n| 27| (A) item 27\n| 2| (B) item 2\n| 28| (B) item 28\n| 3| (C) item 3\n| 29| (C) item 29\n| 4| (D) item 4\n| 30| (D) item 30\n| 5| (E) item 5\n| 31| (E) item 31\n| 6| (F) item 6\n| 32| (F) item 32\n| 7| (G) item 7\n| 33| (G) item 33\n| 8| (H) item 8\n| 34| (H) item 34\n| 9| (I) item 9\n| 35| (I) item 35\n| 10| (J) item 10\n| 36| (J) item 36\n| 11| (K) item 11\n")
self.assertEqual(self.errors, "")
@mock.patch('topydo.commands.ListCommand.get_terminal_size')
def test_list45(self, mock_terminal_size):
"""Test basic 'N' parameter with nine line terminal."""
# have 9 lines on the terminal will print 7 items and leave 2 lines
# for the next prompt
mock_terminal_size.return_value = self.terminal_size(100, 9)
self.todolist = load_file_to_todolist("test/data/ListCommand_50_items.txt")
command = ListCommand(["-N"], self.todolist, self.out, self.error)
command.execute()
self.assertEqual(self.output, "| 1| (A) item 1\n| 27| (A) item 27\n| 2| (B) item 2\n| 28| (B) item 28\n| 3| (C) item 3\n| 29| (C) item 29\n| 4| (D) item 4\n")
self.assertEqual(self.errors, "")
@mock.patch('topydo.commands.ListCommand.get_terminal_size')
def test_list46(self, mock_terminal_size):
"""Test basic 'N' parameter with zero height terminal."""
# we still print at least 1 item
mock_terminal_size.return_value = self.terminal_size(100, 0)
self.todolist = load_file_to_todolist("test/data/ListCommand_50_items.txt")
command = ListCommand(["-N"], self.todolist, self.out, self.error)
command.execute()
self.assertEqual(self.output, "| 1| (A) item 1\n")
self.assertEqual(self.errors, "")
def test_list47(self):
command = ListCommand(["created:2015-11-05"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n") self.assertEqual(self.output, "| 1| (C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project\n")
...@@ -331,7 +397,7 @@ class ListCommandTest(CommandTest): ...@@ -331,7 +397,7 @@ class ListCommandTest(CommandTest):
class ListCommandUnicodeTest(CommandTest): class ListCommandUnicodeTest(CommandTest):
def setUp(self): def setUp(self):
super(ListCommandUnicodeTest, self).setUp() super().setUp()
self.todolist = load_file_to_todolist("test/data/ListCommandUnicodeTest.txt") self.todolist = load_file_to_todolist("test/data/ListCommandUnicodeTest.txt")
def test_list_unicode1(self): def test_list_unicode1(self):
...@@ -340,7 +406,7 @@ class ListCommandUnicodeTest(CommandTest): ...@@ -340,7 +406,7 @@ class ListCommandUnicodeTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
expected = u"| 1| (C) And some sp\u00e9cial tag:\u25c4\n" expected = u"| 1| (C) And some sp\u00e9cial tag:\u25c4\n"
...@@ -354,7 +420,7 @@ class ListCommandJsonTest(CommandTest): ...@@ -354,7 +420,7 @@ class ListCommandJsonTest(CommandTest):
command = ListCommand(["-f", "json"], todolist, self.out, self.error) command = ListCommand(["-f", "json"], todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(todolist.is_dirty()) self.assertFalse(todolist.dirty)
jsontext = "" jsontext = ""
with codecs.open('test/data/ListCommandTest.json', 'r', with codecs.open('test/data/ListCommandTest.json', 'r',
...@@ -370,7 +436,7 @@ class ListCommandJsonTest(CommandTest): ...@@ -370,7 +436,7 @@ class ListCommandJsonTest(CommandTest):
command = ListCommand(["-f", "json"], todolist, self.out, self.error) command = ListCommand(["-f", "json"], todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(todolist.is_dirty()) self.assertFalse(todolist.dirty)
jsontext = "" jsontext = ""
with codecs.open('test/data/ListCommandUnicodeTest.json', 'r', with codecs.open('test/data/ListCommandUnicodeTest.json', 'r',
...@@ -400,7 +466,7 @@ class ListCommandIcalTest(CommandTest): ...@@ -400,7 +466,7 @@ class ListCommandIcalTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertTrue(todolist.is_dirty()) self.assertTrue(todolist.dirty)
icaltext = "" icaltext = ""
with codecs.open('test/data/ListCommandTest.ics', 'r', with codecs.open('test/data/ListCommandTest.ics', 'r',
...@@ -417,7 +483,7 @@ class ListCommandIcalTest(CommandTest): ...@@ -417,7 +483,7 @@ class ListCommandIcalTest(CommandTest):
command = ListCommand(["-f", "ical"], todolist, self.out, self.error) command = ListCommand(["-f", "ical"], todolist, self.out, self.error)
command.execute() command.execute()
self.assertTrue(todolist.is_dirty()) self.assertTrue(todolist.dirty)
icaltext = "" icaltext = ""
with codecs.open('test/data/ListCommandUnicodeTest.ics', 'r', with codecs.open('test/data/ListCommandUnicodeTest.ics', 'r',
......
...@@ -25,16 +25,18 @@ from topydo.commands.ListCommand import ListCommand ...@@ -25,16 +25,18 @@ from topydo.commands.ListCommand import ListCommand
from topydo.lib.Config import config from topydo.lib.Config import config
# We're searching for 'mock' # We're searching for 'mock'
# 'mock' was added as 'unittest.mock' in Python 3.3, but PyPy 3 is based on Python 3.2
# pylint: disable=no-name-in-module # pylint: disable=no-name-in-module
try: try:
from unittest import mock from unittest import mock
except ImportError: except ImportError:
import mock import mock
@freeze_time("2015, 11, 06") @freeze_time("2015, 11, 06")
class ListFormatTest(CommandTest): class ListFormatTest(CommandTest):
def setUp(self): def setUp(self):
super(ListFormatTest, self).setUp() super().setUp()
self.todolist = load_file_to_todolist("test/data/ListFormat.txt") self.todolist = load_file_to_todolist("test/data/ListFormat.txt")
self.terminal_size = namedtuple('terminal_size', ['columns', 'lines']) self.terminal_size = namedtuple('terminal_size', ['columns', 'lines'])
...@@ -43,7 +45,7 @@ class ListFormatTest(CommandTest): ...@@ -43,7 +45,7 @@ class ListFormatTest(CommandTest):
command = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29 result = """| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29
| 2| (Z) 2015-11-06 Lorem ipsum dolorem sit amet. Red @fox +jumped over the and jar due:2015-11-08 lazy:bar t:2015-11-07 | 2| (Z) 2015-11-06 Lorem ipsum dolorem sit amet. Red @fox +jumped over the and jar due:2015-11-08 lazy:bar t:2015-11-07
| 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project | 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project
| 4| (C) Baz @Context1 +Project1 key:value | 4| (C) Baz @Context1 +Project1 key:value
...@@ -60,7 +62,7 @@ class ListFormatTest(CommandTest): ...@@ -60,7 +62,7 @@ class ListFormatTest(CommandTest):
command = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29 result = """| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29
| 2| (Z) 2015-11-06 Lorem ipsum dolore... due:2015-11-08 lazy:bar t:2015-11-07 | 2| (Z) 2015-11-06 Lorem ipsum dolore... due:2015-11-08 lazy:bar t:2015-11-07
| 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project | 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project
| 4| (C) Baz @Context1 +Project1 key:value | 4| (C) Baz @Context1 +Project1 key:value
...@@ -77,7 +79,7 @@ class ListFormatTest(CommandTest): ...@@ -77,7 +79,7 @@ class ListFormatTest(CommandTest):
command = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29 result = """| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29
| 2| (Z) 2015-11-06 Lorem ipsum dolorem sit amet. Red @fox... due:2015-11-08 lazy:bar t:2015-11-07 | 2| (Z) 2015-11-06 Lorem ipsum dolorem sit amet. Red @fox... due:2015-11-08 lazy:bar t:2015-11-07
| 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project | 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project
| 4| (C) Baz @Context1 +Project1 key:value | 4| (C) Baz @Context1 +Project1 key:value
...@@ -94,7 +96,7 @@ class ListFormatTest(CommandTest): ...@@ -94,7 +96,7 @@ class ListFormatTest(CommandTest):
command = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29 result = """| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29
| 2| (Z) 2015-11-06 Lorem ipsum dolorem sit amet. Red @fox... due:2015-11-08 lazy:bar t:2015-11-07 | 2| (Z) 2015-11-06 Lorem ipsum dolorem sit amet. Red @fox... due:2015-11-08 lazy:bar t:2015-11-07
| 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project | 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project
| 4| (C) Baz @Context1 +Project1 key:value | 4| (C) Baz @Context1 +Project1 key:value
...@@ -111,7 +113,7 @@ class ListFormatTest(CommandTest): ...@@ -111,7 +113,7 @@ class ListFormatTest(CommandTest):
command = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29 result = """| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29
| 2| (Z) 2015-11-06 Lorem ipsum dolore... due:2015-11-08 lazy:bar t:2015-11-07 | 2| (Z) 2015-11-06 Lorem ipsum dolore... due:2015-11-08 lazy:bar t:2015-11-07
| 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project | 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project
| 4| (C) Baz @Context1 +Project1 key:value | 4| (C) Baz @Context1 +Project1 key:value
...@@ -119,6 +121,8 @@ class ListFormatTest(CommandTest): ...@@ -119,6 +121,8 @@ class ListFormatTest(CommandTest):
| 6| x 2014-12-12 Completed but with date:2014-12-12 | 6| x 2014-12-12 Completed but with date:2014-12-12
""" """
self.assertEqual(self.output, result)
@mock.patch('topydo.lib.ListFormat.get_terminal_size') @mock.patch('topydo.lib.ListFormat.get_terminal_size')
def test_list_format06(self, mock_terminal_size): def test_list_format06(self, mock_terminal_size):
mock_terminal_size.return_value = self.terminal_size(100, 25) mock_terminal_size.return_value = self.terminal_size(100, 25)
...@@ -127,8 +131,8 @@ class ListFormatTest(CommandTest): ...@@ -127,8 +131,8 @@ class ListFormatTest(CommandTest):
command = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""| 1| D Bar @Context1 +Project2 (3 months ago, due a month ago, started a month ago) 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) | 3| C Foo @Context2 Not@Context +Project1 Not+Project (4 months ago)
| 4| C Baz @Context1 +Project1 key:value | 4| C Baz @Context1 +Project1 key:value
| 5| Drink beer @ home | 5| Drink beer @ home
...@@ -144,7 +148,7 @@ class ListFormatTest(CommandTest): ...@@ -144,7 +148,7 @@ class ListFormatTest(CommandTest):
command = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""| 1| D Bar @Context1 +Project2 (due a month ago, started a month ago) result = """| 1| D Bar @Context1 +Project2 (due a month ago, started a month ago)
| 2| Z Lorem ipsum dolorem sit amet. Red @fox +jumped... lazy:bar (due in 2 days, starts in a day) | 2| Z Lorem ipsum dolorem sit amet. Red @fox +jumped... lazy:bar (due in 2 days, starts in a day)
| 3| C Foo @Context2 Not@Context +Project1 Not+Project | 3| C Foo @Context2 Not@Context +Project1 Not+Project
| 4| C Baz @Context1 +Project1 key:value | 4| C Baz @Context1 +Project1 key:value
...@@ -161,7 +165,7 @@ class ListFormatTest(CommandTest): ...@@ -161,7 +165,7 @@ class ListFormatTest(CommandTest):
command = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""2015-08-31 2015-09-30 2015-09-29 result = """2015-08-31 2015-09-30 2015-09-29
2015-11-06 2015-11-08 2015-11-07 2015-11-06 2015-11-08 2015-11-07
2015-07-12 2015-07-12
...@@ -178,8 +182,8 @@ x 2014-12-12 ...@@ -178,8 +182,8 @@ x 2014-12-12
command = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""3 months ago | a month ago | a month ago | 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 | | | 4 months ago | | |
| | | | | |
| | | | | |
...@@ -192,7 +196,7 @@ just now | in 2 days | in a day | ...@@ -192,7 +196,7 @@ just now | in 2 days | in a day |
command = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""|1| result = """|1|
|2| lazy:bar |2| lazy:bar
|3| |3|
|4| key:value |4| key:value
...@@ -206,7 +210,7 @@ just now | in 2 days | in a day | ...@@ -206,7 +210,7 @@ just now | in 2 days | in a day |
command = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""| 1| due:2015-09-30 t:2015-09-29 result = """| 1| due:2015-09-30 t:2015-09-29
| 2| due:2015-11-08 lazy:bar t:2015-11-07 | 2| due:2015-11-08 lazy:bar t:2015-11-07
| 3| | 3|
| 4| key:value | 4| key:value
...@@ -220,7 +224,7 @@ just now | in 2 days | in a day | ...@@ -220,7 +224,7 @@ just now | in 2 days | in a day |
command = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""| 1| % result = """| 1| %
| 2| % | 2| %
| 3| % | 3| %
| 4| % | 4| %
...@@ -234,7 +238,7 @@ just now | in 2 days | in a day | ...@@ -234,7 +238,7 @@ just now | in 2 days | in a day |
self.todolist, self.out, self.error) self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29 result = """| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29
| 2| (Z) 2015-11-06 Lorem ipsum dolorem sit amet. Red @fox +jumped over the and jar due:2015-11-08 lazy:bar t:2015-11-07 | 2| (Z) 2015-11-06 Lorem ipsum dolorem sit amet. Red @fox +jumped over the and jar due:2015-11-08 lazy:bar t:2015-11-07
| 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project | 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project
| 4| (C) Baz @Context1 +Project1 key:value | 4| (C) Baz @Context1 +Project1 key:value
...@@ -250,7 +254,7 @@ just now | in 2 days | in a day | ...@@ -250,7 +254,7 @@ just now | in 2 days | in a day |
self.todolist, self.out, self.error) self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29 result = """| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29
| 4| (C) Baz @Context1 +Project1 key:value | 4| (C) Baz @Context1 +Project1 key:value
""" """
...@@ -260,7 +264,7 @@ just now | in 2 days | in a day | ...@@ -260,7 +264,7 @@ just now | in 2 days | in a day |
command = ListCommand(["-x", "-F", "%c"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "%c"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""2015-08-31 result = """2015-08-31
2015-11-06 2015-11-06
2015-07-12 2015-07-12
...@@ -273,8 +277,8 @@ just now | in 2 days | in a day | ...@@ -273,8 +277,8 @@ just now | in 2 days | in a day |
command = ListCommand(["-x", "-F", "%C"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "%C"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""3 months ago result = """3 months ago
just now today
4 months ago 4 months ago
...@@ -286,7 +290,7 @@ just now ...@@ -286,7 +290,7 @@ just now
command = ListCommand(["-x", "-F", "%d"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "%d"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""2015-09-30 result = """2015-09-30
2015-11-08 2015-11-08
...@@ -299,7 +303,7 @@ just now ...@@ -299,7 +303,7 @@ just now
command = ListCommand(["-x", "-F", "%D"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "%D"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""a month ago result = """a month ago
in 2 days in 2 days
...@@ -312,7 +316,7 @@ in 2 days ...@@ -312,7 +316,7 @@ in 2 days
command = ListCommand(["-x", "-F", "%h"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "%h"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""due a month ago, started a month ago result = """due a month ago, started a month ago
due in 2 days, starts in a day due in 2 days, starts in a day
...@@ -325,8 +329,8 @@ due in 2 days, starts in a day ...@@ -325,8 +329,8 @@ due in 2 days, starts in a day
command = ListCommand(["-x", "-F", "%H"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "%H"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""3 months ago, due a month ago, started a month ago 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 4 months ago
...@@ -338,7 +342,7 @@ just now, due in 2 days, starts in a day ...@@ -338,7 +342,7 @@ just now, due in 2 days, starts in a day
command = ListCommand(["-x", "-F", "%i"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "%i"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""1 result = """1
2 2
3 3
4 4
...@@ -351,7 +355,7 @@ just now, due in 2 days, starts in a day ...@@ -351,7 +355,7 @@ just now, due in 2 days, starts in a day
command = ListCommand(["-x", "-F", "%I"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "%I"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u""" 1 result = """ 1
2 2
3 3
4 4
...@@ -364,7 +368,7 @@ just now, due in 2 days, starts in a day ...@@ -364,7 +368,7 @@ just now, due in 2 days, starts in a day
command = ListCommand(["-x", "-F", "%k"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "%k"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u""" result = """
lazy:bar lazy:bar
key:value key:value
...@@ -377,7 +381,7 @@ date:2014-12-12 ...@@ -377,7 +381,7 @@ date:2014-12-12
command = ListCommand(["-x", "-F", "%K"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "%K"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""due:2015-09-30 t:2015-09-29 result = """due:2015-09-30 t:2015-09-29
due:2015-11-08 lazy:bar t:2015-11-07 due:2015-11-08 lazy:bar t:2015-11-07
key:value key:value
...@@ -390,7 +394,7 @@ date:2014-12-12 ...@@ -390,7 +394,7 @@ date:2014-12-12
command = ListCommand(["-x", "-F", "%p"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "%p"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""D result = """D
Z Z
C C
C C
...@@ -419,7 +423,7 @@ Completed but with ...@@ -419,7 +423,7 @@ Completed but with
command = ListCommand(["-x", "-F", "%S"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "%S"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""Bar @Context1 +Project2 result = """Bar @Context1 +Project2
Lorem ipsum dolorem sit amet. Red @fox +jumped... Lorem ipsum dolorem sit amet. Red @fox +jumped...
Foo @Context2 Not@Context +Project1 Not+Project Foo @Context2 Not@Context +Project1 Not+Project
Baz @Context1 +Project1 Baz @Context1 +Project1
...@@ -432,7 +436,7 @@ Completed but with ...@@ -432,7 +436,7 @@ Completed but with
command = ListCommand(["-x", "-F", "%t"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "%t"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""2015-09-29 result = """2015-09-29
2015-11-07 2015-11-07
...@@ -445,7 +449,7 @@ Completed but with ...@@ -445,7 +449,7 @@ Completed but with
command = ListCommand(["-x", "-F", "%T"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "%T"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""a month ago result = """a month ago
in a day in a day
...@@ -458,7 +462,7 @@ in a day ...@@ -458,7 +462,7 @@ in a day
command = ListCommand(["-x", "-F", "%x"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "%x"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u""" result = """
...@@ -471,7 +475,7 @@ x 2014-12-12 ...@@ -471,7 +475,7 @@ x 2014-12-12
command = ListCommand(["-x", "-F", "%X"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "%X"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u""" result = """
...@@ -484,7 +488,7 @@ x 11 months ago ...@@ -484,7 +488,7 @@ x 11 months ago
command = ListCommand(["-x", "-s", "desc:priority", "-F", "%{{}p{}}"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-s", "desc:priority", "-F", "%{{}p{}}"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""{C} result = """{C}
{C} {C}
{D} {D}
{Z} {Z}
...@@ -497,7 +501,7 @@ x 11 months ago ...@@ -497,7 +501,7 @@ x 11 months ago
command = ListCommand(["-x", "-s", "desc:priority", "-F", "%{\%p}p{\%p}"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-s", "desc:priority", "-F", "%{\%p}p{\%p}"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""%pC%p result = """%pC%p
%pC%p %pC%p
%pD%p %pD%p
%pZ%p %pZ%p
...@@ -510,7 +514,7 @@ x 11 months ago ...@@ -510,7 +514,7 @@ x 11 months ago
command = ListCommand(["-x", "-s", "desc:priority", "-F", "%p%p"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-s", "desc:priority", "-F", "%p%p"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""CC result = """CC
CC CC
DD DD
ZZ ZZ
...@@ -525,7 +529,7 @@ ZZ ...@@ -525,7 +529,7 @@ ZZ
command = ListCommand(["-x", "-s", "desc:priority", "-F", "%p{ } %{ }p"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-s", "desc:priority", "-F", "%p{ } %{ }p"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""C C result = """C C
C C C C
D D D D
Z Z Z Z
...@@ -541,7 +545,7 @@ Z Z ...@@ -541,7 +545,7 @@ Z Z
command = ListCommand(["-x", "-s", "desc:priority", "-F", "%p{ } %{ }p"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-s", "desc:priority", "-F", "%p{ } %{ }p"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""C C result = """C C
C C C C
D D D D
Z Z Z Z
...@@ -556,7 +560,7 @@ Z Z ...@@ -556,7 +560,7 @@ Z Z
command = ListCommand(["-x", "-s", "desc:priority", "-F", " %{ }p"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-s", "desc:priority", "-F", " %{ }p"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u""" C result = """ C
C C
D D
Z Z
...@@ -572,7 +576,7 @@ Z Z ...@@ -572,7 +576,7 @@ Z Z
command = ListCommand(["-x", "-s", "desc:priority", "-F", "%&"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-s", "desc:priority", "-F", "%&"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u""" result = """
...@@ -589,7 +593,7 @@ Z Z ...@@ -589,7 +593,7 @@ Z Z
command = ListCommand(["-x", "-s", "desc:priority", "-F", "%"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-s", "desc:priority", "-F", "%"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u""" result = """
...@@ -606,7 +610,7 @@ Z Z ...@@ -606,7 +610,7 @@ Z Z
command = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29 result = """| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29
| 2| (Z) 2015-11-06 Lorem ipsum dolorem sit amet. Red @fox... due:2015-11-08 lazy:bar t:2015-11-07 | 2| (Z) 2015-11-06 Lorem ipsum dolorem sit amet. Red @fox... due:2015-11-08 lazy:bar t:2015-11-07
| 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project | 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project
| 4| (C) Baz @Context1 +Project1 key:value | 4| (C) Baz @Context1 +Project1 key:value
...@@ -622,7 +626,7 @@ Z Z ...@@ -622,7 +626,7 @@ Z Z
command = ListCommand(["-x", "-F", "|%I| %x %{(}p{)} %c %S\\t%K"], self.todolist, self.out, self.error) command = ListCommand(["-x", "-F", "|%I| %x %{(}p{)} %c %S\\t%K"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u"""| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29 result = """| 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29
| 2| (Z) 2015-11-06 Lorem ipsum dolorem sit amet. Red @fox... due:2015-11-08 lazy:bar t:2015-11-07 | 2| (Z) 2015-11-06 Lorem ipsum dolorem sit amet. Red @fox... due:2015-11-08 lazy:bar t:2015-11-07
| 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project | 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project
| 4| (C) Baz @Context1 +Project1 key:value | 4| (C) Baz @Context1 +Project1 key:value
...@@ -639,28 +643,50 @@ Z Z ...@@ -639,28 +643,50 @@ Z Z
command = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command.execute() command.execute()
result = u""" | 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29 result = """ | 1| (D) 2015-08-31 Bar @Context1 +Project2 due:2015-09-30 t:2015-09-29
| 2| (Z) 2015-11-06 Lorem ipsum dolorem sit amet. Red @... due:2015-11-08 lazy:bar t:2015-11-07 | 2| (Z) 2015-11-06 Lorem ipsum dolorem sit amet. Red @... due:2015-11-08 lazy:bar t:2015-11-07
| 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project | 3| (C) 2015-07-12 Foo @Context2 Not@Context +Project1 Not+Project
| 4| (C) Baz @Context1 +Project1 key:value | 4| (C) Baz @Context1 +Project1 key:value
| 5| Drink beer @ home ical:foobar id:1 p:2 | 5| Drink beer @ home ical:foobar id:1 p:2
| 6| x 2014-12-12 Completed but with date:2014-12-12 | 6| x 2014-12-12 Completed but with date:2014-12-12
"""
self.assertEqual(self.output, result)
def test_list_format43(self):
command = ListCommand(["-x", "-F", "%P -"], self.todolist, self.out, self.error)
command.execute()
result = """D -
Z -
C -
C -
-
-
"""
self.assertEqual(self.output, result)
def test_list_format44(self):
command = ListCommand(["-x", "-F", "%i %{(}P{)}"], self.todolist, self.out, self.error)
command.execute()
result = """1 (D)
2 (Z)
3 (C)
4 (C)
5 ( )
6 ( )
""" """
self.assertEqual(self.output, result) self.assertEqual(self.output, result)
@mock.patch('topydo.lib.ListFormat.get_terminal_size') @mock.patch('topydo.lib.ListFormat.get_terminal_size')
def test_list_format43(self, mock_terminal_size): def test_list_format45(self, mock_terminal_size):
""" Colorblocks should not affect truncating or right_alignment. """ """ Colorblocks should not affect truncating or right_alignment. """
self.maxDiff = None self.maxDiff = None
mock_terminal_size.return_value = self.terminal_size(100, 25) mock_terminal_size.return_value = self.terminal_size(100, 25)
config(p_overrides={('ls', 'list_format'): '%Z|%I| %x %p %S %k\\t%{(}h{)}'})
command1 = ListCommand(["-x"], self.todolist, self.out, self.error)
command1.execute()
config(p_overrides={('ls', 'list_format'): '%z|%I| %x %p %S %k\\t%{(}h{)}'}) config(p_overrides={('ls', 'list_format'): '%z|%I| %x %p %S %k\\t%{(}h{)}'})
command2 = ListCommand(["-x"], self.todolist, self.out, self.error) command = ListCommand(["-x"], self.todolist, self.out, self.error)
command2.execute() command.execute()
result = u""" | 1| D Bar @Context1 +Project2 (due a month ago, started a month ago) result = u""" | 1| D Bar @Context1 +Project2 (due a month ago, started a month ago)
| 2| Z Lorem ipsum dolorem sit amet. Red @fox +jumpe... lazy:bar (due in 2 days, starts in a day) | 2| Z Lorem ipsum dolorem sit amet. Red @fox +jumpe... lazy:bar (due in 2 days, starts in a day)
...@@ -669,7 +695,21 @@ Z Z ...@@ -669,7 +695,21 @@ Z Z
| 5| Drink beer @ home | 5| Drink beer @ home
| 6| x 2014-12-12 Completed but with date:2014-12-12 | 6| x 2014-12-12 Completed but with date:2014-12-12
""" """
self.assertEqual(self.output, result * 2) 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__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -24,7 +24,7 @@ from topydo.lib.TodoList import TodoList ...@@ -24,7 +24,7 @@ from topydo.lib.TodoList import TodoList
class PostponeCommandTest(CommandTest): class PostponeCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(PostponeCommandTest, self).setUp() super().setUp()
self.today = date.today() self.today = date.today()
self.past = date.today() - timedelta(1) self.past = date.today() - timedelta(1)
self.future = date.today() + timedelta(1) self.future = date.today() + timedelta(1)
...@@ -49,7 +49,7 @@ class PostponeCommandTest(CommandTest): ...@@ -49,7 +49,7 @@ class PostponeCommandTest(CommandTest):
due = self.today + timedelta(7) due = self.today + timedelta(7)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, self.assertEqual(self.output,
"| 1| Foo due:{}\n".format(due.isoformat())) "| 1| Foo due:{}\n".format(due.isoformat()))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -61,7 +61,7 @@ class PostponeCommandTest(CommandTest): ...@@ -61,7 +61,7 @@ class PostponeCommandTest(CommandTest):
due = self.today + timedelta(7) due = self.today + timedelta(7)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, self.assertEqual(self.output,
"| 2| Bar due:{}\n".format(due.isoformat())) "| 2| Bar due:{}\n".format(due.isoformat()))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -73,7 +73,7 @@ class PostponeCommandTest(CommandTest): ...@@ -73,7 +73,7 @@ class PostponeCommandTest(CommandTest):
due = self.today + timedelta(7) due = self.today + timedelta(7)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, self.assertEqual(self.output,
"| 2| Bar due:{}\n".format(due.isoformat())) "| 2| Bar due:{}\n".format(due.isoformat()))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -85,7 +85,7 @@ class PostponeCommandTest(CommandTest): ...@@ -85,7 +85,7 @@ class PostponeCommandTest(CommandTest):
due = self.today + timedelta(7) due = self.today + timedelta(7)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "| 3| Baz due:{} t:{}\n".format(due.isoformat(), self.start.isoformat())) self.assertEqual(self.output, "| 3| Baz due:{} t:{}\n".format(due.isoformat(), self.start.isoformat()))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -97,7 +97,7 @@ class PostponeCommandTest(CommandTest): ...@@ -97,7 +97,7 @@ class PostponeCommandTest(CommandTest):
due = self.today + timedelta(7) due = self.today + timedelta(7)
start = self.start + timedelta(7) start = self.start + timedelta(7)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
# pylint: disable=E1103 # pylint: disable=E1103
self.assertEqual(self.output, "| 3| Baz due:{} t:{}\n".format(due.isoformat(), start.isoformat())) self.assertEqual(self.output, "| 3| Baz due:{} t:{}\n".format(due.isoformat(), start.isoformat()))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -109,7 +109,7 @@ class PostponeCommandTest(CommandTest): ...@@ -109,7 +109,7 @@ class PostponeCommandTest(CommandTest):
due = self.today + timedelta(7) due = self.today + timedelta(7)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, self.assertEqual(self.output,
"| 4| Past due:{}\n".format(due.isoformat())) "| 4| Past due:{}\n".format(due.isoformat()))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -121,7 +121,7 @@ class PostponeCommandTest(CommandTest): ...@@ -121,7 +121,7 @@ class PostponeCommandTest(CommandTest):
due = self.future + timedelta(7) due = self.future + timedelta(7)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
# pylint: disable=E1103 # pylint: disable=E1103
self.assertEqual(self.output, "| 5| Future due:{} t:{}\n".format(due.isoformat(), self.future_start.isoformat())) self.assertEqual(self.output, "| 5| Future due:{} t:{}\n".format(due.isoformat(), self.future_start.isoformat()))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -134,7 +134,7 @@ class PostponeCommandTest(CommandTest): ...@@ -134,7 +134,7 @@ class PostponeCommandTest(CommandTest):
due = self.future + timedelta(7) due = self.future + timedelta(7)
start = self.future_start + timedelta(7) start = self.future_start + timedelta(7)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
# pylint: disable=E1103 # pylint: disable=E1103
self.assertEqual(self.output, "| 5| Future due:{} t:{}\n".format(due.isoformat(), start.isoformat())) self.assertEqual(self.output, "| 5| Future due:{} t:{}\n".format(due.isoformat(), start.isoformat()))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -144,7 +144,7 @@ class PostponeCommandTest(CommandTest): ...@@ -144,7 +144,7 @@ class PostponeCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid date pattern given.\n") self.assertEqual(self.errors, "Invalid date pattern given.\n")
...@@ -153,7 +153,7 @@ class PostponeCommandTest(CommandTest): ...@@ -153,7 +153,7 @@ class PostponeCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
...@@ -162,7 +162,7 @@ class PostponeCommandTest(CommandTest): ...@@ -162,7 +162,7 @@ class PostponeCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
...@@ -170,7 +170,7 @@ class PostponeCommandTest(CommandTest): ...@@ -170,7 +170,7 @@ class PostponeCommandTest(CommandTest):
command = PostponeCommand(["1"], self.todolist, self.out, self.error) command = PostponeCommand(["1"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, command.usage() + "\n") self.assertEqual(self.errors, command.usage() + "\n")
...@@ -181,7 +181,7 @@ class PostponeCommandTest(CommandTest): ...@@ -181,7 +181,7 @@ class PostponeCommandTest(CommandTest):
due = self.today + timedelta(7) due = self.today + timedelta(7)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, self.assertEqual(self.output,
"| 1| Foo due:{}\n".format(due.isoformat())) "| 1| Foo due:{}\n".format(due.isoformat()))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -193,7 +193,7 @@ class PostponeCommandTest(CommandTest): ...@@ -193,7 +193,7 @@ class PostponeCommandTest(CommandTest):
due = self.today + timedelta(7) due = self.today + timedelta(7)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "| 1| Foo due:{}\n| 2| Bar due:{}\n".format(due.isoformat(), due.isoformat())) self.assertEqual(self.output, "| 1| Foo due:{}\n| 2| Bar due:{}\n".format(due.isoformat(), due.isoformat()))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -204,7 +204,7 @@ class PostponeCommandTest(CommandTest): ...@@ -204,7 +204,7 @@ class PostponeCommandTest(CommandTest):
due = self.today + timedelta(7) due = self.today + timedelta(7)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "| 1| Foo due:{}\n| 2| Bar due:{}\n".format(due.isoformat(), due.isoformat())) self.assertEqual(self.output, "| 1| Foo due:{}\n| 2| Bar due:{}\n".format(due.isoformat(), due.isoformat()))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -216,7 +216,7 @@ class PostponeCommandTest(CommandTest): ...@@ -216,7 +216,7 @@ class PostponeCommandTest(CommandTest):
due = self.today + timedelta(7) due = self.today + timedelta(7)
start = self.start + timedelta(7) start = self.start + timedelta(7)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
# pylint: disable=E1103 # pylint: disable=E1103
self.assertEqual(self.output, "| 2| Bar due:{}\n| 3| Baz due:{} t:{}\n".format(due.isoformat(), due.isoformat(), start.isoformat())) self.assertEqual(self.output, "| 2| Bar due:{}\n| 3| Baz due:{} t:{}\n".format(due.isoformat(), due.isoformat(), start.isoformat()))
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -226,7 +226,7 @@ class PostponeCommandTest(CommandTest): ...@@ -226,7 +226,7 @@ class PostponeCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid date pattern given.\n") self.assertEqual(self.errors, "Invalid date pattern given.\n")
...@@ -235,7 +235,7 @@ class PostponeCommandTest(CommandTest): ...@@ -235,7 +235,7 @@ class PostponeCommandTest(CommandTest):
self.out, self.error) self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number given: 99.\nInvalid todo number given: 123.\n") self.assertEqual(self.errors, "Invalid todo number given: 99.\nInvalid todo number given: 123.\n")
...@@ -244,7 +244,7 @@ class PostponeCommandTest(CommandTest): ...@@ -244,7 +244,7 @@ class PostponeCommandTest(CommandTest):
self.out, self.error) self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number given: Zoo.\nInvalid todo number given: 99.\nInvalid todo number given: 123.\n") self.assertEqual(self.errors, "Invalid todo number given: Zoo.\nInvalid todo number given: 99.\nInvalid todo number given: 123.\n")
...@@ -254,7 +254,7 @@ class PostponeCommandTest(CommandTest): ...@@ -254,7 +254,7 @@ class PostponeCommandTest(CommandTest):
self.todolist, self.out, self.error, None) self.todolist, self.out, self.error, None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, self.assertEqual(self.errors,
u"Invalid todo number given: Fo\u00d3B\u0105r.\n") u"Invalid todo number given: Fo\u00d3B\u0105r.\n")
...@@ -267,7 +267,7 @@ class PostponeCommandTest(CommandTest): ...@@ -267,7 +267,7 @@ class PostponeCommandTest(CommandTest):
due = self.today + timedelta(14) due = self.today + timedelta(14)
result = "| 2| Bar due:{d}\n| 3| Baz due:{d} t:{s}\n".format(d=due.isoformat(), s=self.start.isoformat()) result = "| 2| Bar due:{d}\n| 3| Baz due:{d} t:{s}\n".format(d=due.isoformat(), s=self.start.isoformat())
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, result) self.assertEqual(self.output, result)
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -282,7 +282,7 @@ class PostponeCommandTest(CommandTest): ...@@ -282,7 +282,7 @@ class PostponeCommandTest(CommandTest):
result = "| 3| Baz due:{} t:{}\n".format(due.isoformat(), result = "| 3| Baz due:{} t:{}\n".format(due.isoformat(),
self.start.isoformat()) self.start.isoformat())
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, result) self.assertEqual(self.output, result)
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -291,7 +291,7 @@ class PostponeCommandTest(CommandTest): ...@@ -291,7 +291,7 @@ class PostponeCommandTest(CommandTest):
self.todolist, self.out, self.error, None) self.todolist, self.out, self.error, None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
def test_expr_postpone4(self): def test_expr_postpone4(self):
""" Don't postpone unrelevant todo items. """ """ Don't postpone unrelevant todo items. """
...@@ -299,7 +299,7 @@ class PostponeCommandTest(CommandTest): ...@@ -299,7 +299,7 @@ class PostponeCommandTest(CommandTest):
self.out, self.error, None) self.out, self.error, None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
def test_expr_postpone5(self): def test_expr_postpone5(self):
""" Force postponing unrelevant items with additional -x flag. """ """ Force postponing unrelevant items with additional -x flag. """
...@@ -310,7 +310,7 @@ class PostponeCommandTest(CommandTest): ...@@ -310,7 +310,7 @@ class PostponeCommandTest(CommandTest):
due = self.today + timedelta(7) due = self.today + timedelta(7)
result = "| 6| FutureStart t:{} due:{}\n".format(self.future.isoformat(), due.isoformat()) result = "| 6| FutureStart t:{} due:{}\n".format(self.future.isoformat(), due.isoformat())
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, result) self.assertEqual(self.output, result)
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
......
...@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList ...@@ -23,7 +23,7 @@ from topydo.lib.TodoList import TodoList
class PriorityCommandTest(CommandTest): class PriorityCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(PriorityCommandTest, self).setUp() super().setUp()
todos = [ todos = [
"(A) Foo", "(A) Foo",
"Bar", "Bar",
...@@ -39,7 +39,7 @@ class PriorityCommandTest(CommandTest): ...@@ -39,7 +39,7 @@ class PriorityCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, self.assertEqual(self.output,
"Priority changed from A to B\n| 1| (B) Foo\n") "Priority changed from A to B\n| 1| (B) Foo\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -49,7 +49,7 @@ class PriorityCommandTest(CommandTest): ...@@ -49,7 +49,7 @@ class PriorityCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "Priority set to Z.\n| 2| (Z) Bar\n") self.assertEqual(self.output, "Priority set to Z.\n| 2| (Z) Bar\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -58,7 +58,7 @@ class PriorityCommandTest(CommandTest): ...@@ -58,7 +58,7 @@ class PriorityCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, self.assertEqual(self.output,
"Priority changed from A to B\n| 1| (B) Foo\n") "Priority changed from A to B\n| 1| (B) Foo\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -68,7 +68,7 @@ class PriorityCommandTest(CommandTest): ...@@ -68,7 +68,7 @@ class PriorityCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 1| (A) Foo\n") self.assertEqual(self.output, "| 1| (A) Foo\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -77,7 +77,7 @@ class PriorityCommandTest(CommandTest): ...@@ -77,7 +77,7 @@ class PriorityCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "Priority changed from A to C\n| 1| (C) Foo\nPriority set to C.\n| 2| (C) Bar\n") self.assertEqual(self.output, "Priority changed from A to C\n| 1| (C) Foo\nPriority set to C.\n| 2| (C) Bar\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -87,7 +87,17 @@ class PriorityCommandTest(CommandTest): ...@@ -87,7 +87,17 @@ class PriorityCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "Priority changed from A to C\n| 1| (C) Foo\nPriority set to C.\n| 2| (C) Bar\n")
self.assertEqual(self.errors, "")
def test_set_prio7(self):
""" Allow lowercase priority to be set. """
command = PriorityCommand(["Foo", "2", "c"], self.todolist, self.out,
self.error)
command.execute()
self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "Priority changed from A to C\n| 1| (C) Foo\nPriority set to C.\n| 2| (C) Bar\n") self.assertEqual(self.output, "Priority changed from A to C\n| 1| (C) Foo\nPriority set to C.\n| 2| (C) Bar\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -98,7 +108,7 @@ class PriorityCommandTest(CommandTest): ...@@ -98,7 +108,7 @@ class PriorityCommandTest(CommandTest):
result = "Priority changed from B to C\n| 3| (C) a @test with due:2015-06-03\nPriority set to C.\n| 4| (C) a @test with +project p:1\n" result = "Priority changed from B to C\n| 3| (C) a @test with due:2015-06-03\nPriority set to C.\n| 4| (C) a @test with +project p:1\n"
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, result) self.assertEqual(self.output, result)
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -109,7 +119,7 @@ class PriorityCommandTest(CommandTest): ...@@ -109,7 +119,7 @@ class PriorityCommandTest(CommandTest):
result = "Priority changed from B to C\n| 3| (C) a @test with due:2015-06-03\n" result = "Priority changed from B to C\n| 3| (C) a @test with due:2015-06-03\n"
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, result) self.assertEqual(self.output, result)
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -119,7 +129,7 @@ class PriorityCommandTest(CommandTest): ...@@ -119,7 +129,7 @@ class PriorityCommandTest(CommandTest):
None) None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
def test_expr_prio4(self): def test_expr_prio4(self):
""" Don't prioritize unrelevant todo items. """ """ Don't prioritize unrelevant todo items. """
...@@ -127,7 +137,7 @@ class PriorityCommandTest(CommandTest): ...@@ -127,7 +137,7 @@ class PriorityCommandTest(CommandTest):
self.error, None) self.error, None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
def test_expr_prio5(self): def test_expr_prio5(self):
""" Force prioritizing unrelevant items with additional -x flag. """ """ Force prioritizing unrelevant items with additional -x flag. """
...@@ -135,7 +145,7 @@ class PriorityCommandTest(CommandTest): ...@@ -135,7 +145,7 @@ class PriorityCommandTest(CommandTest):
self.error, None) self.error, None)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, self.assertEqual(self.output,
"Priority set to D.\n| 5| (D) Baz id:1\n") "Priority set to D.\n| 5| (D) Baz id:1\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -145,7 +155,7 @@ class PriorityCommandTest(CommandTest): ...@@ -145,7 +155,7 @@ class PriorityCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
...@@ -154,7 +164,7 @@ class PriorityCommandTest(CommandTest): ...@@ -154,7 +164,7 @@ class PriorityCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, "Invalid todo number given: 99.\n") self.assertEqual(self.errors, "Invalid todo number given: 99.\n")
...@@ -163,7 +173,7 @@ class PriorityCommandTest(CommandTest): ...@@ -163,7 +173,7 @@ class PriorityCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, "Invalid todo number given: 98.\nInvalid todo number given: 99.\n") self.assertEqual(self.errors, "Invalid todo number given: 98.\nInvalid todo number given: 99.\n")
...@@ -172,7 +182,7 @@ class PriorityCommandTest(CommandTest): ...@@ -172,7 +182,7 @@ class PriorityCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, "Invalid priority given.\n") self.assertEqual(self.errors, "Invalid priority given.\n")
...@@ -180,7 +190,7 @@ class PriorityCommandTest(CommandTest): ...@@ -180,7 +190,7 @@ class PriorityCommandTest(CommandTest):
command = PriorityCommand(["A"], self.todolist, self.out, self.error) command = PriorityCommand(["A"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, command.usage() + "\n") self.assertEqual(self.errors, command.usage() + "\n")
...@@ -188,7 +198,7 @@ class PriorityCommandTest(CommandTest): ...@@ -188,7 +198,7 @@ class PriorityCommandTest(CommandTest):
command = PriorityCommand(["1"], self.todolist, self.out, self.error) command = PriorityCommand(["1"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, command.usage() + "\n") self.assertEqual(self.errors, command.usage() + "\n")
...@@ -200,7 +210,7 @@ class PriorityCommandTest(CommandTest): ...@@ -200,7 +210,7 @@ class PriorityCommandTest(CommandTest):
self.todolist, self.out, self.error, None) self.todolist, self.out, self.error, None)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, self.assertEqual(self.errors,
u"Invalid todo number given: Fo\u00d3B\u0105r.\n") u"Invalid todo number given: Fo\u00d3B\u0105r.\n")
...@@ -214,7 +224,7 @@ class PriorityCommandTest(CommandTest): ...@@ -214,7 +224,7 @@ class PriorityCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid priority given.\n") self.assertEqual(self.errors, "Invalid priority given.\n")
...@@ -227,7 +237,7 @@ class PriorityCommandTest(CommandTest): ...@@ -227,7 +237,7 @@ class PriorityCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid priority given.\n") self.assertEqual(self.errors, "Invalid priority given.\n")
...@@ -235,7 +245,7 @@ class PriorityCommandTest(CommandTest): ...@@ -235,7 +245,7 @@ class PriorityCommandTest(CommandTest):
command = PriorityCommand([], self.todolist, self.out, self.error) command = PriorityCommand([], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, command.usage() + "\n") self.assertEqual(self.errors, command.usage() + "\n")
......
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2016 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from freezegun import freeze_time
import unittest
from test.topydo_testcase import TopydoTest
from topydo.lib.Config import config
from topydo.lib.ProgressColor import progress_color
from topydo.lib.Todo import Todo
def set_256_colors():
config(p_overrides={('topydo', 'colors'): '256'})
@freeze_time('2016, 01, 01')
class ProgressColorTest(TopydoTest):
def test_progress1(self):
""" Test progress of task with no length """
color = progress_color(Todo('Foo'))
self.assertEqual(color.color, 2)
def test_progress2(self):
""" Test progress of task with no length (but with creation date). """
color = progress_color(Todo('2016-02-11 Foo'))
self.assertEqual(color.color, 2)
def test_progress3(self):
""" Test progress of task with no length """
set_256_colors()
color = progress_color(Todo('Foo'))
self.assertEqual(color.color, 22)
def test_progress4(self):
""" Test progress of task with no length (but with creation date). """
set_256_colors()
color = progress_color(Todo('2016-02-11 Foo'))
self.assertEqual(color.color, 22)
def test_progress5(self):
""" Test overdue tasks """
color = progress_color(Todo('Foo due:2015-12-31'))
self.assertEqual(color.color, 1)
def test_progress6(self):
""" Test overdue tasks """
set_256_colors()
color = progress_color(Todo('Foo due:2015-12-31'))
self.assertEqual(color.color, 196)
def test_progress7(self):
""" Due today """
color = progress_color(Todo('Foo due:2016-01-01'))
self.assertEqual(color.color, 3)
def test_progress8(self):
""" Due today (256) """
set_256_colors()
color = progress_color(Todo('Foo due:2016-01-01'))
self.assertEqual(color.color, 202)
def test_progress9(self):
""" Due tomorrow """
color = progress_color(Todo('Foo due:2016-01-02'))
# a length of 14 days is assumed
self.assertEqual(color.color, 3)
def test_progress10(self):
set_256_colors()
color = progress_color(Todo('Foo due:2016-01-02'))
# a length of 14 days is assumed
self.assertEqual(color.color, 208)
def test_progress11(self):
""" Due tomorrow (creation date) """
color = progress_color(Todo('2016-01-01 Foo due:2016-01-02'))
# a length of 14 days is assumed
self.assertEqual(color.color, 2)
def test_progress12(self):
""" Due tomorrow (creation date) """
set_256_colors()
color = progress_color(Todo('2016-01-01 Foo due:2016-01-02'))
self.assertEqual(color.color, 22)
def test_progress13(self):
""" Due tomorrow (recurrence) """
color = progress_color(Todo('Foo due:2016-01-02 rec:1d'))
self.assertEqual(color.color, 2)
def test_progress14(self):
""" Due tomorrow (recurrence) """
set_256_colors()
color = progress_color(Todo('Foo due:2016-01-02 rec:1d'))
self.assertEqual(color.color, 22)
def test_progress15(self):
""" Due tomorrow (creation date + recurrence) """
color = progress_color(Todo('2016-12-01 Foo due:2016-01-02 rec:1d'))
self.assertEqual(color.color, 2)
def test_progress16(self):
""" Due tomorrow (creation date + recurrence) """
set_256_colors()
color = progress_color(Todo('2015-12-01 Foo due:2016-01-02 rec:1d'))
self.assertEqual(color.color, 22)
def test_progress17(self):
""" Due tomorrow (creation date + recurrence + start date) """
color = progress_color(Todo('2016-12-01 Foo due:2016-01-02 rec:1d t:2016-01-02'))
self.assertEqual(color.color, 2)
def test_progress18(self):
""" Due tomorrow (creation date + recurrence + start date) """
set_256_colors()
color = progress_color(Todo('2015-12-01 Foo due:2016-01-02 rec:1d t:2016-01-02'))
self.assertEqual(color.color, 22)
def test_progress19(self):
""" Due tomorrow (creation date + strict recurrence + start date) """
color = progress_color(Todo('2016-12-01 Foo due:2016-01-02 rec:+1d t:2016-01-02'))
self.assertEqual(color.color, 2)
def test_progress20(self):
""" Due tomorrow (creation date + strict recurrence + start date) """
set_256_colors()
color = progress_color(Todo('2015-12-01 Foo due:2016-01-02 rec:+1d t:2016-01-02'))
self.assertEqual(color.color, 22)
def test_progress21(self):
""" Due tomorrow (creation date + start date) """
color = progress_color(Todo('2016-12-01 Foo due:2016-01-02 t:2016-01-02'))
self.assertEqual(color.color, 2)
def test_progress22(self):
""" Due tomorrow (creation date + start date) """
set_256_colors()
color = progress_color(Todo('2015-12-01 Foo due:2016-01-02 t:2016-01-02'))
self.assertEqual(color.color, 22)
def test_progress23(self):
""" Due tomorrow (creation date + start date) """
color = progress_color(Todo('2016-12-01 Foo due:2016-01-02 t:2015-12-31'))
self.assertEqual(color.color, 10)
def test_progress24(self):
""" Due tomorrow (creation date + start date) """
set_256_colors()
color = progress_color(Todo('2015-12-01 Foo due:2016-01-02 t:2015-12-31'))
self.assertEqual(color.color, 118)
def test_progress25(self):
""" Start date after due date """
color = progress_color(Todo('Foo due:2016-01-02 t:2016-01-03'))
# a length of 14 days is assumed
self.assertEqual(color.color, 3)
def test_progress26(self):
""" Start date after due date """
set_256_colors()
color = progress_color(Todo('Foo due:2016-01-02 t:2016-01-03'))
# a length of 14 days is assumed
self.assertEqual(color.color, 208)
def test_progress27(self):
""" Creation date after due date """
set_256_colors()
color = progress_color(Todo('2016-01-03 Foo due:2016-01-02'))
# a length of 14 days is assumed
self.assertEqual(color.color, 208)
if __name__ == '__main__':
unittest.main()
...@@ -25,7 +25,7 @@ from topydo.lib.Todo import Todo ...@@ -25,7 +25,7 @@ from topydo.lib.Todo import Todo
class RecurrenceTest(TopydoTest): class RecurrenceTest(TopydoTest):
def setUp(self): def setUp(self):
super(RecurrenceTest, self).setUp() super().setUp()
self.todo = Todo("Test rec:1w") self.todo = Todo("Test rec:1w")
self.stricttodo = Todo("Test rec:+1w") self.stricttodo = Todo("Test rec:+1w")
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest import unittest
from datetime import date, timedelta from datetime import date
from freezegun import freeze_time from freezegun import freeze_time
from test.topydo_testcase import TopydoTest from test.topydo_testcase import TopydoTest
...@@ -25,7 +25,7 @@ from topydo.lib.RelativeDate import relative_date_to_date ...@@ -25,7 +25,7 @@ from topydo.lib.RelativeDate import relative_date_to_date
@freeze_time('2015, 11, 06') @freeze_time('2015, 11, 06')
class RelativeDateTester(TopydoTest): class RelativeDateTester(TopydoTest):
def setUp(self): def setUp(self):
super(RelativeDateTester, self).setUp() super().setUp()
self.yesterday = date(2015, 11, 5) self.yesterday = date(2015, 11, 5)
self.today = date(2015, 11, 6) self.today = date(2015, 11, 6)
self.tomorrow = date(2015, 11, 7) self.tomorrow = date(2015, 11, 7)
...@@ -40,6 +40,18 @@ class RelativeDateTester(TopydoTest): ...@@ -40,6 +40,18 @@ class RelativeDateTester(TopydoTest):
result = relative_date_to_date('1d') result = relative_date_to_date('1d')
self.assertEqual(result, self.tomorrow) 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): def test_one_week(self):
result = relative_date_to_date('1w') result = relative_date_to_date('1w')
self.assertEqual(result, date(2015, 11, 13)) self.assertEqual(result, date(2015, 11, 13))
...@@ -152,6 +164,14 @@ class RelativeDateTester(TopydoTest): ...@@ -152,6 +164,14 @@ class RelativeDateTester(TopydoTest):
result = relative_date_to_date('-0d') result = relative_date_to_date('-0d')
self.assertTrue(result, self.today) 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): def test_weekday_next_week(self):
""" """
When entering "Friday" on a Friday, return next week Friday instead of When entering "Friday" on a Friday, return next week Friday instead of
......
...@@ -35,7 +35,7 @@ from topydo.lib.TodoList import TodoList ...@@ -35,7 +35,7 @@ from topydo.lib.TodoList import TodoList
class RevertCommandTest(CommandTest): class RevertCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(RevertCommandTest, self).setUp() super().setUp()
todos = [ todos = [
"Foo", "Foo",
"Bar", "Bar",
......
...@@ -24,7 +24,7 @@ from topydo.lib.Config import config ...@@ -24,7 +24,7 @@ from topydo.lib.Config import config
class SortCommandTest(CommandTest): class SortCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(SortCommandTest, self).setUp() super().setUp()
self.todolist = load_file_to_todolist("test/data/SorterTest1.txt") self.todolist = load_file_to_todolist("test/data/SorterTest1.txt")
def test_sort1(self): def test_sort1(self):
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from freezegun import freeze_time
import unittest import unittest
from test.facilities import (load_file, load_file_to_todolist, print_view, from test.facilities import (load_file, load_file_to_todolist, print_view,
...@@ -23,6 +24,7 @@ from topydo.lib.Config import config ...@@ -23,6 +24,7 @@ from topydo.lib.Config import config
from topydo.lib.Sorter import Sorter from topydo.lib.Sorter import Sorter
@freeze_time("2016, 04, 25")
class SorterTest(TopydoTest): class SorterTest(TopydoTest):
def sort_file(self, p_filename, p_filename_ref, p_sorter): def sort_file(self, p_filename, p_filename_ref, p_sorter):
""" """
...@@ -189,5 +191,14 @@ class SorterTest(TopydoTest): ...@@ -189,5 +191,14 @@ class SorterTest(TopydoTest):
'test/data/SorterTest13-result-context.txt', sorter) 'test/data/SorterTest13-result-context.txt', sorter)
def test_sort19(self):
"""
Check sorting by length.
"""
sorter = Sorter('length,text')
self.sort_file('test/data/SorterTest14.txt',
'test/data/SorterTest14-result.txt', sorter)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -24,7 +24,7 @@ from topydo.lib.TodoList import TodoList ...@@ -24,7 +24,7 @@ from topydo.lib.TodoList import TodoList
class TagCommandTest(CommandTest): class TagCommandTest(CommandTest):
def setUp(self): def setUp(self):
super(TagCommandTest, self).setUp() super().setUp()
todos = [ todos = [
"Foo", "Foo",
"Bar due:2014-10-22", "Bar due:2014-10-22",
...@@ -42,7 +42,7 @@ class TagCommandTest(CommandTest): ...@@ -42,7 +42,7 @@ class TagCommandTest(CommandTest):
self.assertEqual(self.todolist.todo(1).source(), "Foo due:2014-10-22") self.assertEqual(self.todolist.todo(1).source(), "Foo due:2014-10-22")
self.assertEqual(self.output, "| 1| Foo due:2014-10-22\n") self.assertEqual(self.output, "| 1| Foo due:2014-10-22\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
def test_add_tag2(self): def test_add_tag2(self):
command = TagCommand(["Foo", "due", "2014-10-22"], self.todolist, command = TagCommand(["Foo", "due", "2014-10-22"], self.todolist,
...@@ -52,7 +52,7 @@ class TagCommandTest(CommandTest): ...@@ -52,7 +52,7 @@ class TagCommandTest(CommandTest):
self.assertEqual(self.todolist.todo(1).source(), "Foo due:2014-10-22") self.assertEqual(self.todolist.todo(1).source(), "Foo due:2014-10-22")
self.assertEqual(self.output, "| 1| Foo due:2014-10-22\n") self.assertEqual(self.output, "| 1| Foo due:2014-10-22\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
def test_add_tag3(self): def test_add_tag3(self):
command = TagCommand(["-a", "2", "due", "2014-10-19"], self.todolist, command = TagCommand(["-a", "2", "due", "2014-10-19"], self.todolist,
...@@ -64,23 +64,34 @@ class TagCommandTest(CommandTest): ...@@ -64,23 +64,34 @@ class TagCommandTest(CommandTest):
self.assertEqual(self.output, self.assertEqual(self.output,
"| 2| Bar due:2014-10-22 due:2014-10-19\n") "| 2| Bar due:2014-10-22 due:2014-10-19\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
def test_add_tag4(self): def test_add_tag4(self):
command = TagCommand(["Foox", "due", "2014-10-22"], self.todolist, command = TagCommand(["Foox", "due", "2014-10-22"], self.todolist,
self.out, self.error) self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertFalse(self.output) self.assertFalse(self.output)
self.assertEqual(self.errors, "Invalid todo number.\n") self.assertEqual(self.errors, "Invalid todo number.\n")
def test_force_add_tag01(self):
'''Tries to different values to a tag for the same name 3 times.'''
for letter in ['a', 'b', 'c']:
command = TagCommand(['-a', '1', 'k', letter], self.todolist,
self.out, self.error)
command.execute()
self.assertEqual(self.errors, "")
self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.todo(1).source(), "Foo k:a k:b k:c")
def test_set_tag04(self): def test_set_tag04(self):
command = TagCommand(["3", "due", "2014-10-20"], self.todolist, command = TagCommand(["3", "due", "2014-10-20"], self.todolist,
self.out, self.error) self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 3| Baz due:2014-10-20\n") self.assertEqual(self.output, "| 3| Baz due:2014-10-20\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -89,7 +100,7 @@ class TagCommandTest(CommandTest): ...@@ -89,7 +100,7 @@ class TagCommandTest(CommandTest):
self.out, self.error, lambda t: "all") self.out, self.error, lambda t: "all")
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord due:2014-10-20 due:2014-10-20\n") self.assertEqual(self.output, " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord due:2014-10-20 due:2014-10-20\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -98,7 +109,7 @@ class TagCommandTest(CommandTest): ...@@ -98,7 +109,7 @@ class TagCommandTest(CommandTest):
self.out, self.error, lambda t: "1") self.out, self.error, lambda t: "1")
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord due:2014-10-20 due:2014-10-22\n") self.assertEqual(self.output, " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord due:2014-10-20 due:2014-10-22\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -107,7 +118,7 @@ class TagCommandTest(CommandTest): ...@@ -107,7 +118,7 @@ class TagCommandTest(CommandTest):
self.out, self.error, lambda t: "2") self.out, self.error, lambda t: "2")
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord due:2014-10-20 due:2014-10-20\n") self.assertEqual(self.output, " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord due:2014-10-20 due:2014-10-20\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -116,7 +127,7 @@ class TagCommandTest(CommandTest): ...@@ -116,7 +127,7 @@ class TagCommandTest(CommandTest):
self.out, self.error, lambda t: "") self.out, self.error, lambda t: "")
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord due:2014-10-20 due:2014-10-22\n") self.assertEqual(self.output, " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord due:2014-10-20 due:2014-10-22\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -125,7 +136,7 @@ class TagCommandTest(CommandTest): ...@@ -125,7 +136,7 @@ class TagCommandTest(CommandTest):
self.out, self.error, lambda t: "99") self.out, self.error, lambda t: "99")
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord due:2014-10-20 due:2014-10-22\n") self.assertEqual(self.output, " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord due:2014-10-20 due:2014-10-22\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -134,7 +145,7 @@ class TagCommandTest(CommandTest): ...@@ -134,7 +145,7 @@ class TagCommandTest(CommandTest):
self.out, self.error, lambda t: "99") self.out, self.error, lambda t: "99")
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, self.assertEqual(self.output,
"| 4| Fnord due:2014-10-20 due:2014-10-20\n") "| 4| Fnord due:2014-10-20 due:2014-10-20\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -145,7 +156,7 @@ class TagCommandTest(CommandTest): ...@@ -145,7 +156,7 @@ class TagCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "| 3| Baz due:2015-11-19\n") self.assertEqual(self.output, "| 3| Baz due:2015-11-19\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -158,7 +169,7 @@ class TagCommandTest(CommandTest): ...@@ -158,7 +169,7 @@ class TagCommandTest(CommandTest):
self.error) self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "| 3| Baz due:2014-10-20 foo:today\n") self.assertEqual(self.output, "| 3| Baz due:2014-10-20 foo:today\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -166,7 +177,7 @@ class TagCommandTest(CommandTest): ...@@ -166,7 +177,7 @@ class TagCommandTest(CommandTest):
command = TagCommand(["1", "due"], self.todolist, self.out, self.error) command = TagCommand(["1", "due"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "| 1| Foo\n") self.assertEqual(self.output, "| 1| Foo\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -174,7 +185,7 @@ class TagCommandTest(CommandTest): ...@@ -174,7 +185,7 @@ class TagCommandTest(CommandTest):
command = TagCommand(["2", "due"], self.todolist, self.out, self.error) command = TagCommand(["2", "due"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "| 2| Bar\n") self.assertEqual(self.output, "| 2| Bar\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -183,7 +194,7 @@ class TagCommandTest(CommandTest): ...@@ -183,7 +194,7 @@ class TagCommandTest(CommandTest):
self.error, lambda t: "all") self.error, lambda t: "all")
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, self.assertEqual(self.output,
" 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord\n") " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -193,7 +204,7 @@ class TagCommandTest(CommandTest): ...@@ -193,7 +204,7 @@ class TagCommandTest(CommandTest):
lambda t: "1") lambda t: "1")
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord due:2014-10-22\n") self.assertEqual(self.output, " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord due:2014-10-22\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -202,7 +213,7 @@ class TagCommandTest(CommandTest): ...@@ -202,7 +213,7 @@ class TagCommandTest(CommandTest):
lambda t: "99") lambda t: "99")
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord due:2014-10-20 due:2014-10-22\n") self.assertEqual(self.output, " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord due:2014-10-20 due:2014-10-22\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -211,7 +222,7 @@ class TagCommandTest(CommandTest): ...@@ -211,7 +222,7 @@ class TagCommandTest(CommandTest):
lambda t: "A") lambda t: "A")
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord due:2014-10-20 due:2014-10-22\n") self.assertEqual(self.output, " 1. 2014-10-20\n 2. 2014-10-22\n| 4| Fnord due:2014-10-20 due:2014-10-22\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -219,7 +230,7 @@ class TagCommandTest(CommandTest): ...@@ -219,7 +230,7 @@ class TagCommandTest(CommandTest):
command = TagCommand(["5", "due"], self.todolist, self.out, self.error) command = TagCommand(["5", "due"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number.\n") self.assertEqual(self.errors, "Invalid todo number.\n")
...@@ -227,7 +238,7 @@ class TagCommandTest(CommandTest): ...@@ -227,7 +238,7 @@ class TagCommandTest(CommandTest):
command = TagCommand(["A", "due"], self.todolist, self.out, self.error) command = TagCommand(["A", "due"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid todo number.\n") self.assertEqual(self.errors, "Invalid todo number.\n")
...@@ -236,7 +247,7 @@ class TagCommandTest(CommandTest): ...@@ -236,7 +247,7 @@ class TagCommandTest(CommandTest):
self.error, lambda t: "A") self.error, lambda t: "A")
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertEqual(self.output, "| 4| Fnord\n") self.assertEqual(self.output, "| 4| Fnord\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -244,7 +255,7 @@ class TagCommandTest(CommandTest): ...@@ -244,7 +255,7 @@ class TagCommandTest(CommandTest):
command = TagCommand(["4"], self.todolist, self.out, self.error) command = TagCommand(["4"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, command.usage() + "\n") self.assertEqual(self.errors, command.usage() + "\n")
......
...@@ -29,7 +29,7 @@ from topydo.lib.TodoListBase import InvalidTodoException, TodoListBase ...@@ -29,7 +29,7 @@ from topydo.lib.TodoListBase import InvalidTodoException, TodoListBase
class TodoListTester(TopydoTest): class TodoListTester(TopydoTest):
def setUp(self): def setUp(self):
super(TodoListTester, self).setUp() super().setUp()
self.todofile = TodoFile('test/data/TodoListTest.txt') self.todofile = TodoFile('test/data/TodoListTest.txt')
lines = [line for line in self.todofile.read() lines = [line for line in self.todofile.read()
...@@ -40,12 +40,12 @@ class TodoListTester(TopydoTest): ...@@ -40,12 +40,12 @@ class TodoListTester(TopydoTest):
def test_contexts(self): def test_contexts(self):
self.assertEqual(set(['Context1', 'Context2']), self.assertEqual(set(['Context1', 'Context2']),
self.todolist.contexts()) self.todolist.contexts())
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
def test_projects(self): def test_projects(self):
self.assertEqual(set(['Project1', 'Project2']), self.assertEqual(set(['Project1', 'Project2']),
self.todolist.projects()) self.todolist.projects())
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
def test_add1(self): def test_add1(self):
text = "(C) Adding a new task @Context3 +Project3" text = "(C) Adding a new task @Context3 +Project3"
...@@ -58,7 +58,7 @@ class TodoListTester(TopydoTest): ...@@ -58,7 +58,7 @@ class TodoListTester(TopydoTest):
self.assertEqual(set(['Context1', 'Context2', 'Context3']), self.assertEqual(set(['Context1', 'Context2', 'Context3']),
self.todolist.contexts()) self.todolist.contexts())
self.assertEqual(self.todolist.number(todo), 6) self.assertEqual(self.todolist.number(todo), 6)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
def test_add2(self): def test_add2(self):
text = str(self.todolist) text = str(self.todolist)
...@@ -101,7 +101,7 @@ class TodoListTester(TopydoTest): ...@@ -101,7 +101,7 @@ class TodoListTester(TopydoTest):
self.assertEqual(self.todolist.todo(2).source(), self.assertEqual(self.todolist.todo(2).source(),
"(C) Baz @Context1 +Project1 key:value") "(C) Baz @Context1 +Project1 key:value")
self.assertEqual(self.todolist.count(), count - 1) self.assertEqual(self.todolist.count(), count - 1)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
self.assertRaises(InvalidTodoException, self.todolist.number, todo) self.assertRaises(InvalidTodoException, self.todolist.number, todo)
def test_delete2(self): def test_delete2(self):
...@@ -112,7 +112,7 @@ class TodoListTester(TopydoTest): ...@@ -112,7 +112,7 @@ class TodoListTester(TopydoTest):
self.todolist.delete(todo) self.todolist.delete(todo)
self.assertEqual(self.todolist.count(), count) self.assertEqual(self.todolist.count(), count)
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
def test_append1(self): def test_append1(self):
todo = self.todolist.todo(3) todo = self.todolist.todo(3)
...@@ -122,7 +122,7 @@ class TodoListTester(TopydoTest): ...@@ -122,7 +122,7 @@ class TodoListTester(TopydoTest):
"(C) Baz @Context1 +Project1 key:value @Context3") "(C) Baz @Context1 +Project1 key:value @Context3")
self.assertEqual(set(['Context1', 'Context2', 'Context3']), self.assertEqual(set(['Context1', 'Context2', 'Context3']),
self.todolist.contexts()) self.todolist.contexts())
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
def test_append2(self): def test_append2(self):
todo = self.todolist.todo(3) todo = self.todolist.todo(3)
...@@ -145,7 +145,7 @@ class TodoListTester(TopydoTest): ...@@ -145,7 +145,7 @@ class TodoListTester(TopydoTest):
self.assertRaises(InvalidTodoException, self.todolist.todo, self.assertRaises(InvalidTodoException, self.todolist.todo,
count + 100) count + 100)
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
def test_count(self): def test_count(self):
""" Test that empty lines are not counted. """ """ Test that empty lines are not counted. """
...@@ -167,26 +167,26 @@ class TodoListTester(TopydoTest): ...@@ -167,26 +167,26 @@ class TodoListTester(TopydoTest):
todo = self.todolist.todo(1) todo = self.todolist.todo(1)
self.todolist.set_todo_completed(todo) self.todolist.set_todo_completed(todo)
self.assertTrue(self.todolist.todo(1).is_completed()) self.assertTrue(self.todolist.todo(1).is_completed())
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
def test_todo_priority1(self): def test_todo_priority1(self):
todo = self.todolist.todo(1) todo = self.todolist.todo(1)
self.todolist.set_priority(todo, 'F') self.todolist.set_priority(todo, 'F')
self.assertEqual(self.todolist.todo(1).priority(), 'F') self.assertEqual(self.todolist.todo(1).priority(), 'F')
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
def test_todo_priority2(self): def test_todo_priority2(self):
todo = self.todolist.todo(1) todo = self.todolist.todo(1)
self.todolist.set_priority(todo, 'C') self.todolist.set_priority(todo, 'C')
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.dirty)
def test_erase(self): def test_erase(self):
self.todolist.erase() self.todolist.erase()
self.assertEqual(self.todolist.count(), 0) self.assertEqual(self.todolist.count(), 0)
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.dirty)
def test_regex1(self): def test_regex1(self):
""" Multiple hits should result in None. """ """ Multiple hits should result in None. """
...@@ -219,6 +219,13 @@ class TodoListTester(TopydoTest): ...@@ -219,6 +219,13 @@ class TodoListTester(TopydoTest):
config("test/data/todolist-uid.conf") config("test/data/todolist-uid.conf")
self.assertRaises(InvalidTodoException, self.todolist.todo, 1) 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): def test_new_uid(self):
""" Make sure that item has new text ID after append. """ """ Make sure that item has new text ID after append. """
config("test/data/todolist-uid.conf") config("test/data/todolist-uid.conf")
...@@ -227,10 +234,23 @@ class TodoListTester(TopydoTest): ...@@ -227,10 +234,23 @@ class TodoListTester(TopydoTest):
self.assertNotEqual(self.todolist.number(todo), 't5c') 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): class TodoListDependencyTester(TopydoTest):
def setUp(self): def setUp(self):
super(TodoListDependencyTester, self).setUp() super().setUp()
self.todolist = TodoList([]) self.todolist = TodoList([])
self.todolist.add("Foo id:1") self.todolist.add("Foo id:1")
...@@ -242,6 +262,7 @@ class TodoListDependencyTester(TopydoTest): ...@@ -242,6 +262,7 @@ class TodoListDependencyTester(TopydoTest):
self.todolist.add("Another one with +Project") self.todolist.add("Another one with +Project")
self.todolist.add("Todo with +AnotherProject") self.todolist.add("Todo with +AnotherProject")
self.todolist.add("Todo without children id:3") self.todolist.add("Todo without children id:3")
self.todolist.add("Orphan p:4")
def test_check_dep(self): def test_check_dep(self):
children = self.todolist.children(self.todolist.todo(1)) children = self.todolist.children(self.todolist.todo(1))
...@@ -272,8 +293,8 @@ class TodoListDependencyTester(TopydoTest): ...@@ -272,8 +293,8 @@ class TodoListDependencyTester(TopydoTest):
todo5 = self.todolist.todo(5) todo5 = self.todolist.todo(5)
self.todolist.add_dependency(todo5, todo4) self.todolist.add_dependency(todo5, todo4)
self.assertTrue(todo5.has_tag('id', '4')) self.assertTrue(todo5.has_tag('id', '5'))
self.assertTrue(todo4.has_tag('p', '4')) self.assertTrue(todo4.has_tag('p', '5'))
def test_add_dep2(self): def test_add_dep2(self):
""" """
...@@ -287,8 +308,8 @@ class TodoListDependencyTester(TopydoTest): ...@@ -287,8 +308,8 @@ class TodoListDependencyTester(TopydoTest):
self.todolist.add_dependency(todo5, todo4) self.todolist.add_dependency(todo5, todo4)
self.todolist.add_dependency(todo4, todo1) self.todolist.add_dependency(todo4, todo1)
self.assertTrue(todo4.has_tag('id', '5')) self.assertTrue(todo4.has_tag('id', '6'))
self.assertTrue(todo1.has_tag('p', '5')) self.assertTrue(todo1.has_tag('p', '6'))
def test_add_dep3(self): def test_add_dep3(self):
""" """
...@@ -322,6 +343,7 @@ class TodoListDependencyTester(TopydoTest): ...@@ -322,6 +343,7 @@ class TodoListDependencyTester(TopydoTest):
self.assertFalse(from_todo.has_tag('id')) self.assertFalse(from_todo.has_tag('id'))
self.assertFalse(to_todo.has_tag('p')) self.assertFalse(to_todo.has_tag('p'))
self.assertFalse(self.todolist.todo_by_dep_id('2'))
def test_remove_dep2(self): def test_remove_dep2(self):
old = str(self.todolist) old = str(self.todolist)
...@@ -330,6 +352,9 @@ class TodoListDependencyTester(TopydoTest): ...@@ -330,6 +352,9 @@ class TodoListDependencyTester(TopydoTest):
self.todolist.remove_dependency(from_todo, to_todo) self.todolist.remove_dependency(from_todo, to_todo)
self.assertEqual(str(self.todolist), old) 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): def test_remove_dep3(self):
""" Try to remove non-existing dependency. """ """ Try to remove non-existing dependency. """
...@@ -339,6 +364,9 @@ class TodoListDependencyTester(TopydoTest): ...@@ -339,6 +364,9 @@ class TodoListDependencyTester(TopydoTest):
self.todolist.remove_dependency(from_todo, to_todo) self.todolist.remove_dependency(from_todo, to_todo)
self.assertEqual(str(self.todolist), old) 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): def test_remove_todo_check_children(self):
todo = self.todolist.todo(2) todo = self.todolist.todo(2)
...@@ -351,6 +379,7 @@ class TodoListDependencyTester(TopydoTest): ...@@ -351,6 +379,7 @@ class TodoListDependencyTester(TopydoTest):
todo = self.todolist.todo(3) todo = self.todolist.todo(3)
self.todolist.delete(todo) self.todolist.delete(todo)
self.assertFalse(todo.has_tag('p', '2')) self.assertFalse(todo.has_tag('p', '2'))
self.assertFalse(self.todolist.todo_by_dep_id('2'))
todo = self.todolist.todo(1) todo = self.todolist.todo(1)
children = self.todolist.children(todo) children = self.todolist.children(todo)
...@@ -372,6 +401,44 @@ class TodoListDependencyTester(TopydoTest): ...@@ -372,6 +401,44 @@ class TodoListDependencyTester(TopydoTest):
self.assertTrue(todolist.todo_by_dep_id('1')) self.assertTrue(todolist.todo_by_dep_id('1'))
self.assertFalse(todolist.todo_by_dep_id('2')) 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'))
def test_delete01(self):
""" Check that dependency tags are cleaned up. """
todo = self.todolist.todo(4)
self.todolist.delete(todo, p_leave_tags=False)
self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.todo(3).source(), "Baz p:1")
def test_delete02(self):
""" Check that dependency tags are left when requested. """
todo = self.todolist.todo(4)
self.todolist.delete(todo, p_leave_tags=True)
self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.todo(3).source(), "Baz p:1 id:2")
def test_delete03(self):
""" Check that dependency tags are left when requested. """
todo = self.todolist.todo(3)
self.todolist.delete(todo, p_leave_tags=True)
self.assertTrue(self.todolist.dirty)
self.assertEqual(self.todolist.todo(3).source(), "Buzz p:2")
class TodoListCleanDependencyTester(TopydoTest): class TodoListCleanDependencyTester(TopydoTest):
""" """
...@@ -383,7 +450,7 @@ class TodoListCleanDependencyTester(TopydoTest): ...@@ -383,7 +450,7 @@ class TodoListCleanDependencyTester(TopydoTest):
""" """
def setUp(self): def setUp(self):
super(TodoListCleanDependencyTester, self).setUp() super().setUp()
self.todolist = TodoList([]) self.todolist = TodoList([])
def test_clean_dependencies1(self): def test_clean_dependencies1(self):
...@@ -419,19 +486,7 @@ class TodoListCleanDependencyTester(TopydoTest): ...@@ -419,19 +486,7 @@ class TodoListCleanDependencyTester(TopydoTest):
self.todolist.clean_dependencies() self.todolist.clean_dependencies()
self.assertFalse(self.todolist.todo(1).has_tag('id')) self.assertFalse(self.todolist.todo(1).has_tag('id'))
self.assertFalse(self.todolist.todo_by_dep_id('1'))
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'))
if __name__ == '__main__': if __name__ == '__main__':
......
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
from test.topydo_testcase import TopydoTest
from topydo.lib.Utils import translate_key_to_config
class UtilsTest(TopydoTest):
def test_key_to_cfg(self):
ctrl_s = translate_key_to_config('ctrl s')
meta_d = translate_key_to_config('meta d')
esc = translate_key_to_config('esc')
f4 = translate_key_to_config('f4')
self.assertEqual(ctrl_s, '<C-s>')
self.assertEqual(meta_d, '<M-d>')
self.assertEqual(esc, '<Esc>')
self.assertEqual(f4, '<F4>')
if __name__ == '__main__':
unittest.main()
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
default_command = ls default_command = ls
; filename = todo.txt ; filename = todo.txt
; archive_filename = done.txt ; archive_filename = done.txt
colors = 1 colors = auto
; identifiers can be 'linenumber' or 'text' ; identifiers can be 'linenumber' or 'text'
identifiers = linenumber identifiers = linenumber
backup_count = 5 backup_count = 5
...@@ -42,6 +42,8 @@ append_parent_contexts = 0 ...@@ -42,6 +42,8 @@ append_parent_contexts = 0
; [light-]gray, darkgray or numbers from 0 to 255. When number is specified color ; [light-]gray, darkgray or numbers from 0 to 255. When number is specified color
; is matched from Xterm color chart available here: ; is matched from Xterm color chart available here:
; http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg ; http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg
; When using values between 16 and 256, make sure to set colors = 256 in the
; [topydo] section.
; priority_colors = A:cyan,B:yellow,C:blue ; priority_colors = A:cyan,B:yellow,C:blue
; project_color = red ; project_color = red
...@@ -52,7 +54,9 @@ append_parent_contexts = 0 ...@@ -52,7 +54,9 @@ append_parent_contexts = 0
[aliases] [aliases]
;showall = ls -x ;showall = ls -x
;next = ls -n 1 ;next = ls -n 1
;top = ls -F '|%I| %x %p %S %k %{(}H{)}' ;top = ls -F '%I %x %P %S %k %{(}H{)}' -N
;star = tag {} star 1
;unstar = tag {} star
;lsproj = lsprj ;lsproj = lsprj
;listprj = lsprj ;listprj = lsprj
;listproj = lsprj ;listproj = lsprj
...@@ -61,3 +65,35 @@ append_parent_contexts = 0 ...@@ -61,3 +65,35 @@ append_parent_contexts = 0
;listcon = lscon ;listcon = lscon
;listcontext = lscon ;listcontext = lscon
;listcontexts = lscon ;listcontexts = lscon
[columns]
column_width = 40
[column_keymap]
; Keymap configuration for column-mode
gg = home
G = end
j = down
k = up
d = cmd del {}
e = cmd edit {}
u = cmd revert
x = cmd do {}
pp = postpone
ps = postpone_s
pr = pri
m = mark
0 = first_column
$ = last_column
h = prev_column
l = next_column
A = append_column
I = insert_column
E = edit_column
D = delete_column
Y = copy_column
L = swap_left
R = swap_right
<Left> = prev_column
<Right> = next_column
<Esc> = reset
...@@ -21,7 +21,7 @@ instance based on an argument list. ...@@ -21,7 +21,7 @@ instance based on an argument list.
import sys import sys
from topydo.lib.Config import config from topydo.lib.Config import config, ConfigError
_SUBCOMMAND_MAP = { _SUBCOMMAND_MAP = {
'add': 'AddCommand', 'add': 'AddCommand',
...@@ -71,6 +71,18 @@ def get_subcommand(p_args): ...@@ -71,6 +71,18 @@ def get_subcommand(p_args):
__import__(modulename, globals(), locals(), [classname], 0) __import__(modulename, globals(), locals(), [classname], 0)
return getattr(sys.modules[modulename], classname) return getattr(sys.modules[modulename], classname)
def join_args(p_cli_args, p_alias_args):
"""
Returns properly joined args from alias definition and from user input.
"""
if '{}' in p_alias_args:
pos = p_alias_args.index('{}')
args = p_alias_args[:pos] + p_cli_args + p_alias_args[pos+1:]
else:
args = p_alias_args + p_cli_args
return args
def resolve_alias(p_alias, p_args): def resolve_alias(p_alias, p_args):
""" """
Resolves a subcommand alias and returns a tuple (Command, args). Resolves a subcommand alias and returns a tuple (Command, args).
...@@ -78,10 +90,14 @@ def get_subcommand(p_args): ...@@ -78,10 +90,14 @@ def get_subcommand(p_args):
If alias resolves to non-existent command, main help message is If alias resolves to non-existent command, main help message is
returned. 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: try:
result = import_subcommand(real_subcommand) result = import_subcommand(real_subcommand)
args = alias_args + p_args args = join_args(p_args, alias_args)
return (result, args) return (result, args)
except KeyError: except KeyError:
return get_subcommand(['help']) return get_subcommand(['help'])
......
...@@ -22,19 +22,17 @@ from datetime import date ...@@ -22,19 +22,17 @@ from datetime import date
from os.path import expanduser from os.path import expanduser
from sys import stdin from sys import stdin
from topydo.lib.Command import Command from topydo.lib.WriteCommand import WriteCommand
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers
from topydo.lib.RelativeDate import relative_date_to_date
from topydo.lib.TodoListBase import InvalidTodoException
class AddCommand(Command): class AddCommand(WriteCommand):
def __init__(self, p_args, p_todolist, # pragma: no branch def __init__(self, p_args, p_todolist, # pragma: no branch
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(AddCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.text = ' '.join(p_args) self.text = ' '.join(p_args)
self.from_file = None self.from_file = None
...@@ -70,56 +68,18 @@ class AddCommand(Command): ...@@ -70,56 +68,18 @@ class AddCommand(Command):
return todo_text return todo_text
def _postprocess_input_todo(p_todo):
"""
Post-processes a parsed todo when adding it to the list.
* It converts relative dates to absolute ones.
* Automatically inserts a creation date if not present.
* Handles more user-friendly dependencies with before:, partof: and
after: tags
"""
def convert_date(p_tag):
value = p_todo.tag_value(p_tag)
if value:
dateobj = relative_date_to_date(value)
if dateobj:
p_todo.set_tag(p_tag, dateobj.isoformat())
def add_dependencies(p_tag):
for value in p_todo.tag_values(p_tag):
try:
dep = self.todolist.todo(value)
if p_tag == 'after':
self.todolist.add_dependency(p_todo, dep)
elif p_tag == 'before' or p_tag == 'partof':
self.todolist.add_dependency(dep, p_todo)
except InvalidTodoException:
pass
p_todo.remove_tag(p_tag, value)
convert_date(config().tag_start())
convert_date(config().tag_due())
add_dependencies('partof')
add_dependencies('before')
add_dependencies('after')
if config().auto_creation_date():
p_todo.set_creation_date(date.today())
todo_text = _preprocess_input_todo(p_todo_text) todo_text = _preprocess_input_todo(p_todo_text)
todo = self.todolist.add(todo_text) todo = self.todolist.add(todo_text)
_postprocess_input_todo(todo) self.postprocess_input_todo(todo)
if config().auto_creation_date():
todo.set_creation_date(date.today())
self.out(self.printer.print_todo(todo)) self.out(self.printer.print_todo(todo))
def execute(self): def execute(self):
""" Adds a todo item to the list. """ """ Adds a todo item to the list. """
if not super(AddCommand, self).execute(): if not super().execute():
return False return False
self.printer.add_filter(PrettyPrinterNumbers(self.todolist)) self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
...@@ -138,23 +98,22 @@ class AddCommand(Command): ...@@ -138,23 +98,22 @@ class AddCommand(Command):
def usage(self): def usage(self):
return """Synopsis: return """Synopsis:
add <text> add <TEXT>
add -f <file> add -f <FILE> | -"""
add -f -"""
def help(self): def help(self):
return """\ return """\
This subcommand automatically adds the creation date to the added item. This subcommand automatically adds the creation date to the added item.
<text> may contain: TEXT may contain:
* Priorities mid-sentence. Example: add "Water flowers (C)" * Priorities mid-sentence. Example: add "Water flowers (C)"
* Dependencies using before, after and partof tags. They are translated to the * Dependencies using before, after, partof, parents-of and children-of tags.
corresponding 'id' and 'p' tags. The values of these tags correspond to the These are translated to the corresponding 'id' and 'p' tags. The values of
todo number (not the dependency number). these tags correspond to the todo number (not the dependency number).
Example: add "Subtask partof:1" Example: add "Subtask partof:1"
-f : Add todo items from specified <file> or from standard input. -f : Add todo items from specified FILE or from standard input.\
""" """
...@@ -14,21 +14,23 @@ ...@@ -14,21 +14,23 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.Command import Command, InvalidCommandArgument from topydo.lib.Config import config
from topydo.lib.Command import InvalidCommandArgument
from topydo.lib.WriteCommand import WriteCommand
from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers
from topydo.lib.TodoListBase import InvalidTodoException from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.TodoParser import parse_line
class AppendCommand(WriteCommand):
class AppendCommand(Command):
def __init__(self, p_args, p_todolist, #pragma: no branch def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=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) p_prompt)
def execute(self): def execute(self):
if not super(AppendCommand, self).execute(): if not super().execute():
return False return False
try: try:
...@@ -37,7 +39,13 @@ class AppendCommand(Command): ...@@ -37,7 +39,13 @@ class AppendCommand(Command):
if text: if text:
todo = self.todolist.todo(number) todo = self.todolist.todo(number)
new_text_parsed = parse_line(text)
new_tags = new_text_parsed['tags']
for tag in (config().tag_start(), config().tag_due()):
if tag in new_tags:
todo.remove_tag(tag)
self.todolist.append(todo, text) self.todolist.append(todo, text)
self.postprocess_input_todo(todo)
self.printer.add_filter(PrettyPrinterNumbers(self.todolist)) self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
self.out(self.printer.print_todo(todo)) self.out(self.printer.print_todo(todo))
...@@ -49,9 +57,9 @@ class AppendCommand(Command): ...@@ -49,9 +57,9 @@ class AppendCommand(Command):
self.error("Invalid todo number given.") self.error("Invalid todo number given.")
def usage(self): def usage(self):
return """Synopsis: append <number> <text>""" return """Synopsis: append <NUMBER> <TEXT>"""
def help(self): def help(self):
return """\ return """\
Adds the given <text> to the end of the todo indicated by <number>. Adds the given TEXT to the end of the todo indicated by NUMBER.\
""" """
...@@ -27,7 +27,7 @@ class ArchiveCommand(Command): ...@@ -27,7 +27,7 @@ class ArchiveCommand(Command):
TodoListBase class which does no dependency checking, so a better TodoListBase class which does no dependency checking, so a better
choice for huge done.txt files. choice for huge done.txt files.
""" """
super(ArchiveCommand, self).__init__([], p_todolist) super().__init__([], p_todolist)
self.archive = p_archive_list self.archive = p_archive_list
def execute(self): def execute(self):
......
...@@ -22,7 +22,7 @@ class DeleteCommand(DCommand): ...@@ -22,7 +22,7 @@ class DeleteCommand(DCommand):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(DeleteCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
def prompt_text(self): def prompt_text(self):
...@@ -40,15 +40,15 @@ class DeleteCommand(DCommand): ...@@ -40,15 +40,15 @@ class DeleteCommand(DCommand):
def usage(self): def usage(self):
return """\ return """\
Synopsis: del [-f] <NUMBER1> [<NUMBER2> ...] Synopsis: del [-f] <NUMBER 1> [<NUMBER 2> ...]
del [-x] -e <EXPRESSION> del [-x] -e <EXPRESSION>\
""" """
def help(self): def help(self):
return """\ return """\
Deletes the todo item(s) with the given number(s) from the list. Deletes the todo item(s) with the given number(s) from the list.
It is also possible to delete items as complete with an expression using It is also possible to delete items that match EXPRESSION using the -e flag.
the -e flag. Use -x to also process todo items that are normally invisible Use -x to also process todo items that are normally invisible (as with the 'ls'
(with the 'ls' subcommand). subcommand).\
""" """
...@@ -29,7 +29,7 @@ class DepCommand(Command): ...@@ -29,7 +29,7 @@ class DepCommand(Command):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(DepCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
try: try:
...@@ -40,44 +40,66 @@ class DepCommand(Command): ...@@ -40,44 +40,66 @@ class DepCommand(Command):
self.printer = pretty_printer_factory(self.todolist) self.printer = pretty_printer_factory(self.todolist)
def _handle_add(self): def _handle_add(self):
(from_todo, to_todo) = self._get_todos() for from_todo, to_todo in self._get_todos():
if from_todo and to_todo:
self.todolist.add_dependency(from_todo, to_todo) self.todolist.add_dependency(from_todo, to_todo)
def _handle_rm(self): def _handle_rm(self):
(from_todo, to_todo) = self._get_todos() for from_todo, to_todo in self._get_todos():
if from_todo and to_todo:
self.todolist.remove_dependency(from_todo, to_todo) self.todolist.remove_dependency(from_todo, to_todo)
def _get_todos(self): def _get_todos(self):
from_todo = None result = []
to_todo = None
def get_parent_dependencies():
child_todo = first_todo
sibling_todo = second_todo
return [(parent, child_todo) for parent in self.todolist.parents(sibling_todo)]
def get_child_dependencies():
parent_todo = first_todo
sibling_todo = second_todo
return [(parent_todo, child) for child in self.todolist.children(sibling_todo)]
get_before_dependency = lambda: [(second_todo, first_todo)]
get_after_dependency = lambda: [(first_todo, second_todo)]
operators = {
"after": get_after_dependency,
"before": get_before_dependency,
"child-of": get_child_dependencies,
"childof": get_child_dependencies,
"children-of": get_child_dependencies,
"childrenof": get_child_dependencies,
"parent-of": get_parent_dependencies,
"parentof": get_parent_dependencies,
"parents-of": get_parent_dependencies,
"parentsof": get_parent_dependencies,
"partof": get_before_dependency,
"to": get_after_dependency,
}
try: try:
first_todo_nr = self.argument(1)
operator = self.argument(2) operator = self.argument(2)
if operator == 'before' or operator == 'partof': if operator in operators:
from_todo_nr = self.argument(3) second_todo_nr = self.argument(3)
to_todo_nr = self.argument(1)
elif operator == 'to' or operator == 'after':
from_todo_nr = self.argument(1)
to_todo_nr = self.argument(3)
else: else:
# the operator was omitted, assume 2nd argument is target task second_todo_nr = self.argument(2)
# default to 'to' behavior operator = "to"
from_todo_nr = self.argument(1)
to_todo_nr = self.argument(2) first_todo = self.todolist.todo(first_todo_nr)
second_todo = self.todolist.todo(second_todo_nr)
from_todo = self.todolist.todo(from_todo_nr) result = operators[operator]()
to_todo = self.todolist.todo(to_todo_nr)
except (InvalidTodoException): except (InvalidTodoException):
self.error("Invalid todo number given.") self.error("Invalid todo number given.")
except InvalidCommandArgument: except InvalidCommandArgument:
self.error(self.usage()) self.error(self.usage())
return (from_todo, to_todo) return result
def _handle_ls(self): def _handle_ls(self):
""" Handles the ls subsubcommand. """ """ Handles the ls subsubcommand. """
...@@ -127,7 +149,7 @@ class DepCommand(Command): ...@@ -127,7 +149,7 @@ class DepCommand(Command):
def execute(self): def execute(self):
if not super(DepCommand, self).execute(): if not super().execute():
return False return False
dispatch = { dispatch = {
...@@ -148,7 +170,7 @@ class DepCommand(Command): ...@@ -148,7 +170,7 @@ class DepCommand(Command):
def usage(self): def usage(self):
return """Synopsis: return """Synopsis:
dep <add|rm> <NUMBER> [to] <NUMBER> dep <add|rm> <NUMBER> [to] <NUMBER>
dep add <NUMBER> <before|partof|after> <NUMBER> dep add <NUMBER> <before|partof|after|parents-of|children-of> <NUMBER>
dep ls <NUMBER> to dep ls <NUMBER> to
dep ls to <NUMBER> dep ls to <NUMBER>
dep dot <NUMBER> dep dot <NUMBER>
...@@ -156,10 +178,11 @@ class DepCommand(Command): ...@@ -156,10 +178,11 @@ class DepCommand(Command):
def help(self): def help(self):
return """\ return """\
* add : Adds a dependency. Using 1 before 2 creates a dependency * add : Adds a dependency. `dep add 1 2` denotes that todo item 1
from todo item 2 to 1. is dependant on todo item 2, i.e. item 2 is a subitem of
item 1.
* rm (alias: del) : Removes a dependency. * rm (alias: del) : Removes a dependency.
* ls : Lists all dependencies to or from a certain todo. * ls : Lists all dependencies to or from a certain todo.
* dot : Prints a dependency tree as a Dot graph. * dot : Prints a dependency tree as a Dot graph.
* clean (alias: gc) : Removes redundant id or p tags. * clean (alias: gc) : Removes redundant id or p tags.\
""" """
...@@ -23,7 +23,7 @@ class DepriCommand(MultiCommand): ...@@ -23,7 +23,7 @@ class DepriCommand(MultiCommand):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(DepriCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
def _execute_multi_specific(self): def _execute_multi_specific(self):
...@@ -37,14 +37,14 @@ class DepriCommand(MultiCommand): ...@@ -37,14 +37,14 @@ class DepriCommand(MultiCommand):
def usage(self): def usage(self):
return """\ return """\
Synopsis: depri <NUMBER1> [<NUMBER2> ...] Synopsis: depri <NUMBER 1> [<NUMBER 2> ...]
depri [-x] -e <EXPRESSION> depri [-x] -e <EXPRESSION>\
""" """
def help(self): def help(self):
return """Removes the priority of the given todo item(s). return """Removes the priority of the given todo item(s).
It is also possible to deprioritize items as complete with an expression using It is also possible to deprioritize items as complete with an EXPRESSION using
the -e flag. Use -x to also process todo items that are normally invisible the -e flag. Use -x to also process todo items that are normally invisible (as
(with the 'ls' subcommand). with the 'ls' subcommand).\
""" """
...@@ -33,17 +33,17 @@ class DoCommand(DCommand): ...@@ -33,17 +33,17 @@ class DoCommand(DCommand):
self.strict_recurrence = False self.strict_recurrence = False
self.completion_date = date.today() self.completion_date = date.today()
super(DoCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
def get_flags(self): def get_flags(self):
""" Additional flags. """ """ Additional flags. """
opts, long_opts = super(DoCommand, self).get_flags() opts, long_opts = super().get_flags()
return ("d:s" + opts, ["date=", "strict"] + long_opts) return ("d:s" + opts, ["date=", "strict"] + long_opts)
def process_flag(self, p_opt, p_value): 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": if p_opt == "-s" or p_opt == "--strict":
self.strict_recurrence = True self.strict_recurrence = True
...@@ -105,28 +105,28 @@ class DoCommand(DCommand): ...@@ -105,28 +105,28 @@ class DoCommand(DCommand):
def usage(self): def usage(self):
return """\ return """\
Synopsis: do [--date] [--force] [--strict] <NUMBER1> [<NUMBER2> ...] Synopsis: do [--date <DATE>] [--force] [--strict] <NUMBER 1> [<NUMBER 2> ...]
do [-x] -e <EXPRESSION> do [-x] -e <EXPRESSION>\
""" """
def help(self): def help(self):
return """Marks the todo(s) with given number(s) as complete. return """Marks the todo(s) with given NUMBER(s) as complete.
It is also possible to mark todo items as complete with an expression using the It is also possible to mark todo items as complete with an EXPRESSION using the
-e flag. Use -x to also process todo items that are normally invisible (with -e flag. Use -x to also process todo items that are normally invisible (as with
the 'ls' subcommand). the 'ls' subcommand).
In case a todo has subitems, a question is asked whether the subitems should be In case a todo has subitems (dependencies), a question is asked whether the
marked as completed as well. When --force is given, no interaction is required subitems should be marked as completed as well. When --force is given, no
and the subitems are not marked completed. interaction is required and the subitems are not marked completed.
In case a completed todo is recurring, a new todo will be added to the list, In case a completed todo is recurring, a new todo will be added to the list,
while the given todo item is marked as complete. The new date is calculated while the given todo item is marked as complete. The new date is calculated
based on the todo item's due date. If the due date is in the past, today's date based on the todo item's due date. If the due date is in the past, today's date
is used to calculate the new recurrence date. Using --strict prevents this, is used to calculate the new recurrence date. Using --strict prevents this, and
then the actual due date of the todo item is used to calculate the new then the actual due date of the todo item is used to calculate the new
recurrence date. Note that a future due date is always used as such to recurrence date. Note that a future due date is always used as such to
calculate the new due date. calculate the new due date.
Use --date to set a custom completion date. Use --date to set a custom completion date.\
""" """
...@@ -27,10 +27,6 @@ from topydo.lib.TodoList import TodoList ...@@ -27,10 +27,6 @@ from topydo.lib.TodoList import TodoList
# the true and only editor # the true and only editor
DEFAULT_EDITOR = 'vi' DEFAULT_EDITOR = 'vi'
# Access the base class of the TodoList instance kept inside EditCommand. We
# cannot use super() inside the class itself
BASE_TODOLIST = lambda tl: super(TodoList, tl)
def _get_file_mtime(p_file): def _get_file_mtime(p_file):
return os.stat(p_file.name).st_mtime return os.stat(p_file.name).st_mtime
...@@ -39,7 +35,7 @@ def _is_edited(p_orig_mtime, p_file): ...@@ -39,7 +35,7 @@ def _is_edited(p_orig_mtime, p_file):
class EditCommand(MultiCommand): class EditCommand(MultiCommand):
def __init__(self, p_args, p_todolist, p_output, p_error, p_input): 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) p_error, p_input)
if len(self.args) == 0: if len(self.args) == 0:
...@@ -115,7 +111,7 @@ class EditCommand(MultiCommand): ...@@ -115,7 +111,7 @@ class EditCommand(MultiCommand):
if _is_edited(orig_mtime, temp_todos): if _is_edited(orig_mtime, temp_todos):
for todo in self.todos: for todo in self.todos:
BASE_TODOLIST(self.todolist).delete(todo) self.todolist.delete(todo, p_leave_tags=True)
for todo in new_todos: for todo in new_todos:
self.todolist.add_todo(todo) self.todolist.add_todo(todo)
...@@ -138,8 +134,8 @@ class EditCommand(MultiCommand): ...@@ -138,8 +134,8 @@ class EditCommand(MultiCommand):
def usage(self): def usage(self):
return """Synopsis: return """Synopsis:
edit edit
edit <NUMBER1> [<NUMBER2> ...] edit <NUMBER 1> [<NUMBER 2> ...]
edit -e [-x] [expression] edit -e [-x] [EXPRESSION]
edit -d""" edit -d"""
def help(self): def help(self):
...@@ -147,15 +143,15 @@ class EditCommand(MultiCommand): ...@@ -147,15 +143,15 @@ class EditCommand(MultiCommand):
Launches a text editor to edit todos. Launches a text editor to edit todos.
Without any arguments it will just open the todo.txt file. Alternatively it can Without any arguments it will just open the todo.txt file. Alternatively it can
edit todo item(s) with the given number(s) or edit relevant todos matching edit todo item(s) with the given NUMBER(s) or edit relevant todos matching
the given expression. See `topydo help ls` for more information on relevant the given EXPRESSION. See `topydo help ls` for more information on relevant
todo items. It is also possible to open the archive file. todo items. It is also possible to open the archive file.
By default it will use $EDITOR in your environment, otherwise it will fall back By default it will look to your environment variable $EDITOR, otherwise it will
to 'vi'. fall back to 'vi'.
-e : Treat the subsequent arguments as an expression. -e : Treat the subsequent arguments as an EXPRESSION.
-x : Edit *all* todos matching the expression (i.e. do not filter on -x : Edit *all* todos matching the EXPRESSION (i.e. do not filter on
dependencies or relevance). dependencies or relevance).
-d : Open the archive file. -d : Open the archive file.\
""" """
...@@ -26,11 +26,11 @@ class ExitCommand(Command): ...@@ -26,11 +26,11 @@ class ExitCommand(Command):
""" """
def __init__(self, p_args, p_todolist, p_output, p_error, p_input): 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) p_input)
def execute(self): def execute(self):
if not super(ExitCommand, self).execute(): if not super().execute():
return False return False
sys.exit(0) sys.exit(0)
...@@ -20,6 +20,7 @@ from topydo.lib.Filter import InstanceFilter ...@@ -20,6 +20,7 @@ from topydo.lib.Filter import InstanceFilter
from topydo.lib.printers.PrettyPrinter import pretty_printer_factory from topydo.lib.printers.PrettyPrinter import pretty_printer_factory
from topydo.lib.prettyprinters.Format import PrettyPrinterFormatFilter from topydo.lib.prettyprinters.Format import PrettyPrinterFormatFilter
from topydo.lib.TodoListBase import InvalidTodoException from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.Utils import get_terminal_size
class ListCommand(ExpressionCommand): class ListCommand(ExpressionCommand):
...@@ -27,7 +28,7 @@ class ListCommand(ExpressionCommand): ...@@ -27,7 +28,7 @@ class ListCommand(ExpressionCommand):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(ListCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.printer = None self.printer = None
...@@ -50,7 +51,7 @@ class ListCommand(ExpressionCommand): ...@@ -50,7 +51,7 @@ class ListCommand(ExpressionCommand):
return True return True
def _process_flags(self): def _process_flags(self):
opts, args = self.getopt('f:F:i:n:s:x') opts, args = self.getopt('f:F:i:n:Ns:x')
for opt, value in opts: for opt, value in opts:
if opt == '-x': if opt == '-x':
...@@ -65,22 +66,26 @@ class ListCommand(ExpressionCommand): ...@@ -65,22 +66,26 @@ class ListCommand(ExpressionCommand):
if self._poke_icalendar(): if self._poke_icalendar():
from topydo.lib.printers.Ical import IcalPrinter from topydo.lib.printers.Ical import IcalPrinter
self.printer = IcalPrinter(self.todolist) self.printer = IcalPrinter(self.todolist)
# a graph without dependencies is not so useful, hence
# show all
self.show_all = True
elif value == 'dot': elif value == 'dot':
from topydo.lib.printers.Dot import DotPrinter from topydo.lib.printers.Dot import DotPrinter
self.printer = DotPrinter(self.todolist) self.printer = DotPrinter(self.todolist)
# a graph without dependencies is not so useful, hence
# show all
self.show_all = True
else: else:
self.printer = None self.printer = None
elif opt == '-F': elif opt == '-F':
self.format = value self.format = value
elif opt == '-N':
# 2 lines are assumed to be taken up by printing the next prompt
# display at least one item
self.limit = max(get_terminal_size().lines - 2, 1)
elif opt == '-n': elif opt == '-n':
try: try:
self.limit = int(value) self.limit = int(value)
except ValueError: except ValueError:
pass # use default value in configuration pass # use default value in configuration
elif opt == '-i': elif opt == '-i':
self.ids = value.split(',') self.ids = value.split(',')
...@@ -94,7 +99,7 @@ class ListCommand(ExpressionCommand): ...@@ -94,7 +99,7 @@ class ListCommand(ExpressionCommand):
Additional filters to select particular todo items given with the -i Additional filters to select particular todo items given with the -i
flag. flag.
""" """
filters = super(ListCommand, self)._filters() filters = super()._filters()
if self.ids: if self.ids:
def get_todo(p_id): def get_todo(p_id):
...@@ -124,7 +129,6 @@ class ListCommand(ExpressionCommand): ...@@ -124,7 +129,6 @@ class ListCommand(ExpressionCommand):
# create a standard printer with some filters # create a standard printer with some filters
indent = config().list_indent() indent = config().list_indent()
final_format = ' ' * indent + self.format final_format = ' ' * indent + self.format
hidden_tags = config().hidden_tags()
filters = [] filters = []
filters.append(PrettyPrinterFormatFilter(self.todolist, final_format)) filters.append(PrettyPrinterFormatFilter(self.todolist, final_format))
...@@ -134,7 +138,7 @@ class ListCommand(ExpressionCommand): ...@@ -134,7 +138,7 @@ class ListCommand(ExpressionCommand):
self.out(self.printer.print_list(self._view().todos)) self.out(self.printer.print_list(self._view().todos))
def execute(self): def execute(self):
if not super(ListCommand, self).execute(): if not super().execute():
return False return False
try: try:
...@@ -148,20 +152,21 @@ class ListCommand(ExpressionCommand): ...@@ -148,20 +152,21 @@ class ListCommand(ExpressionCommand):
return True return True
def usage(self): def usage(self):
return """Synopsis: ls [-x] [-s <sort_expression>] [-f <output format>] return """Synopsis: ls [-x] [-s <SORT EXPRESSION>] [-f <OUTPUT FORMAT>]
[-F <format string>] [expression]""" [-F <FORMAT STRING>] [-i <NUMBER 1>[,<NUMBER 2> ...]] [-N | -n <INTEGER>]
[EXPRESSION]"""
def help(self): def help(self):
return """\ return """\
Lists all relevant todos. A todo is relevant when: Lists all relevant todos. A todo is relevant when:
* has not been completed yet; * has not been completed yet,
* the start date (if present) has passed; * the start date (if present) has passed, and
* there are no subitems that need to be completed. * there are no subitems that need to be completed.
When an expression is given, only the todos matching that expression are shown. When an EXPRESSION is given, only the todos matching that EXPRESSION are shown.
-f : Specify the output format, being 'text' (default), 'dot' or 'ical' or -f : Specify the OUTPUT format, being 'text' (default), 'dot' or 'ical' or
'json'. 'json'.
* 'text' - Text output with colors and indentation if applicable. * 'text' - Text output with colors and indentation if applicable.
...@@ -172,6 +177,7 @@ When an expression is given, only the todos matching that expression are shown. ...@@ -172,6 +177,7 @@ When an expression is given, only the todos matching that expression are shown.
an 'ical' tag with a unique ID. Completed todo items may be an 'ical' tag with a unique ID. Completed todo items may be
archived. archived.
* 'json' - Javascript Object Notation (JSON) * 'json' - Javascript Object Notation (JSON)
-F : Specify the format of the text ('text' format), which may contain -F : Specify the format of the text ('text' format), which may contain
placeholders that may be expanded if the todo has such attribute. If such placeholders that may be expanded if the todo has such attribute. If such
attribute does not exist, then it expands to an empty string. attribute does not exist, then it expands to an empty string.
...@@ -187,6 +193,7 @@ When an expression is given, only the todos matching that expression are shown. ...@@ -187,6 +193,7 @@ When an expression is given, only the todos matching that expression are shown.
%k: List of tags separated by spaces (excluding hidden tags). %k: List of tags separated by spaces (excluding hidden tags).
%K: List of all tags separated by spaces. %K: List of all tags separated by spaces.
%p: Priority. %p: Priority.
%P: Priority or placeholder space if no priority.
%s: Todo text. %s: Todo text.
%S: Todo text, truncated such that an item fits on one line. %S: Todo text, truncated such that an item fits on one line.
%t: Absolute creation date. %t: Absolute creation date.
...@@ -198,12 +205,14 @@ When an expression is given, only the todos matching that expression are shown. ...@@ -198,12 +205,14 @@ When an expression is given, only the todos matching that expression are shown.
Conditional characters can be added with blocks surrounded by curly Conditional characters can be added with blocks surrounded by curly
braces, they will only appear when a placeholder expanded to a value. braces, they will only appear when a placeholder expanded to a value.
E.g. %{(}p{)} will print (C) when the todo item has priority C, or '' E.g. %{(}p{)} will print '(C)' when the todo item has priority C, or ''
(empty string) when an item has no priority set. (empty string) when an item has no priority set.
A tab character serves as a marker to start right alignment. A tab character serves as a marker to start right alignment.
-i : Comma separated list of todo IDs to print. -i : Comma separated list of todo IDs to print.
-s : Sort the list according to a sort expression. Defaults to the expression -n : Number of items to display. Defaults to the value in the configuration.
-N : Limit number of items displayed such that they fit on the terminal.
-s : Sort the list according to a SORT EXPRESSION. Defaults to the expression
in the configuration. in the configuration.
-x : Show all todos (i.e. do not filter on dependencies or relevance). -x : Show all todos (i.e. do not filter on dependencies or relevance).\
""" """
...@@ -22,11 +22,11 @@ class ListContextCommand(Command): ...@@ -22,11 +22,11 @@ class ListContextCommand(Command):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(ListContextCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
def execute(self): def execute(self):
if not super(ListContextCommand, self).execute(): if not super().execute():
return False return False
for context in sorted(self.todolist.contexts(), key=lambda s: s.lower()): for context in sorted(self.todolist.contexts(), key=lambda s: s.lower()):
......
...@@ -22,11 +22,11 @@ class ListProjectCommand(Command): ...@@ -22,11 +22,11 @@ class ListProjectCommand(Command):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(ListProjectCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
def execute(self): def execute(self):
if not super(ListProjectCommand, self).execute(): if not super().execute():
return False return False
for project in sorted(self.todolist.projects(), key=lambda s: s.lower()): for project in sorted(self.todolist.projects(), key=lambda s: s.lower()):
......
...@@ -28,7 +28,7 @@ class PostponeCommand(MultiCommand): ...@@ -28,7 +28,7 @@ class PostponeCommand(MultiCommand):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(PostponeCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.move_start_date = False self.move_start_date = False
...@@ -69,7 +69,7 @@ class PostponeCommand(MultiCommand): ...@@ -69,7 +69,7 @@ class PostponeCommand(MultiCommand):
# pylint: disable=E1103 # pylint: disable=E1103
todo.set_tag(config().tag_due(), new_due.isoformat()) todo.set_tag(config().tag_due(), new_due.isoformat())
self.todolist.set_dirty() self.todolist.dirty = True
self.out(self.printer.print_todo(todo)) self.out(self.printer.print_todo(todo))
else: else:
self.error("Invalid date pattern given.") self.error("Invalid date pattern given.")
...@@ -77,23 +77,23 @@ class PostponeCommand(MultiCommand): ...@@ -77,23 +77,23 @@ class PostponeCommand(MultiCommand):
def usage(self): def usage(self):
return """\ return """\
Synopsis: postpone [-s] <NUMBER> [<NUMBER2> ...] <PATTERN>" Synopsis: postpone [-s] <NUMBER> [<NUMBER2> ...] <PATTERN>
postpone [-x] -e <EXPRESSION> postpone [-x] -e <EXPRESSION>\
""" """
def help(self): def help(self):
return """\ return """\
Postpone the todo item(s) with the given number(s) and the given pattern. Postpone the todo item(s) with the given NUMBER(s) and the given PATTERN.
Postponing is done by adjusting the due date(s) of the todo(s), and if the -s Postponing is done by adjusting the due date(s) of the todo(s), and if the -s
flag is given, the start date accordingly. flag is given, the start date accordingly.
It is also possible to postpone items as complete with an expression using It is also possible to postpone items as complete with an EXPRESSION using
the -e flag. Use -x to also process todo items that are normally invisible the -e flag. Use -x to also process todo items that are normally invisible (as
(with the 'ls' subcommand). with the 'ls' subcommand).
The pattern is a relative date, written in the format <COUNT><PERIOD> where The PATTERN is a relative date, written in the format <COUNT> <PERIOD> where
count is a number and <PERIOD> is either 'd', 'w', 'm' or 'y', which stands for COUNT is a number and PERIOD is either 'd', 'w', 'm' or 'y', which stands for
days, weeks, months and years respectively. Example: 'postpone 1 1w' postpones days, weeks, months and years respectively. Example: 'postpone 1 2w' postpones
todo number 1 for 1 week. todo number 1 for 2 weeks.\
""" """
...@@ -22,18 +22,18 @@ from topydo.lib.Utils import is_valid_priority ...@@ -22,18 +22,18 @@ from topydo.lib.Utils import is_valid_priority
class PriorityCommand(MultiCommand): class PriorityCommand(MultiCommand):
def __init__(self, p_args, p_todolist, #pragma: no branch def __init__(self, p_args, p_todolist, # pragma: no branch
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(PriorityCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.last_argument = True self.last_argument = True
def _execute_multi_specific(self): def _execute_multi_specific(self):
def normalize_priority(p_priority): def normalize_priority(p_priority):
match = re.search(r'\b([A-Z])\b', p_priority) match = re.search(r'\b([A-Z])\b', p_priority.upper())
return match.group(1) if match else p_priority return match.group(1) if match else p_priority
priority = normalize_priority(self.args[-1]) priority = normalize_priority(self.args[-1])
...@@ -56,15 +56,15 @@ class PriorityCommand(MultiCommand): ...@@ -56,15 +56,15 @@ class PriorityCommand(MultiCommand):
def usage(self): def usage(self):
return """\ return """\
Synopsis: pri <NUMBER1> [<NUMBER2> ...] <PRIORITY> Synopsis: pri <NUMBER 1> [<NUMBER 2> ...] <PRIORITY>
pri [-x] -e <EXPRESSION> pri [-x] -e <EXPRESSION>\
""" """
def help(self): def help(self):
return """\ return """\
Sets the priority of todo(s) the given number(s) to the given priority. Sets the priority of todo(s) the given NUMBER(s) to the given PRIORITY.
It is also possible to prioritize items as complete with an expression using It is also possible to prioritize items with an EXPRESSION using the -e flag.
the -e flag. Use -x to also process todo items that are normally invisible Use -x to also process todo items that are normally invisible (as with the 'ls'
(with the 'ls' subcommand). subcommand).\
""" """
...@@ -25,11 +25,11 @@ class RevertCommand(Command): ...@@ -25,11 +25,11 @@ class RevertCommand(Command):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=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) p_prompt)
def execute(self): def execute(self):
if not super(RevertCommand, self).execute(): if not super().execute():
return False return False
archive_file = TodoFile.TodoFile(config().archive()) archive_file = TodoFile.TodoFile(config().archive())
...@@ -54,6 +54,4 @@ class RevertCommand(Command): ...@@ -54,6 +54,4 @@ class RevertCommand(Command):
return """Synopsis: revert""" return """Synopsis: revert"""
def help(self): def help(self):
return """\ return """Reverts the last command."""
Reverts the last command.
"""
...@@ -24,11 +24,11 @@ class SortCommand(Command): ...@@ -24,11 +24,11 @@ class SortCommand(Command):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(SortCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
def execute(self): def execute(self):
if not super(SortCommand, self).execute(): if not super().execute():
return False return False
try: try:
...@@ -42,14 +42,14 @@ class SortCommand(Command): ...@@ -42,14 +42,14 @@ class SortCommand(Command):
self.todolist.replace(sorted_todos) self.todolist.replace(sorted_todos)
def usage(self): def usage(self):
return """Synopsis: sort [expression]""" return """Synopsis: sort [<EXPRESSION>]"""
def help(self): def help(self):
return """\ return """\
Sorts the file according to the expression. If no expression is given, the Sorts the todo file according to the EXPRESSION. If no EXPRESSION is given, the
expression in the configuration is used. expression in the configuration is used.
The expression is a comma separated list of attributes to sort on. The list is The EXPRESSION is a comma separated list of attributes to sort on. The list is
evaluated in order, which means that the first attribute takes higher evaluated in order, which means that the first attribute takes higher
precedence, then the second, etc. precedence, then the second, etc.
...@@ -61,12 +61,12 @@ The following sort attributes are supported: ...@@ -61,12 +61,12 @@ The following sort attributes are supported:
* importance - Sort by importance * importance - Sort by importance
* importance-avg - Sort by average importance (based on parent items) * importance-avg - Sort by average importance (based on parent items)
* text - Sort by text * text - Sort by text
* <tag> - Sort by values of the given tag * <TAG> - Sort by values of the given TAG
Each item can optionally be prefixed with asc: and desc: to specify ascending Each item can optionally be prefixed with asc: and desc: to specify ascending
or descending sort respectively. If not specified, ascending sort is assumed. or descending sort respectively. If not specified, ascending sort is assumed.
Example: Example:
desc:importance,due,desc:priority desc:importance,due,desc:priority\
""" """
...@@ -26,7 +26,7 @@ class TagCommand(Command): ...@@ -26,7 +26,7 @@ class TagCommand(Command):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(TagCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.force = False self.force = False
...@@ -106,10 +106,10 @@ class TagCommand(Command): ...@@ -106,10 +106,10 @@ class TagCommand(Command):
self.todo.set_tag(self.tag, self.value, self.force_add, p_old_value) self.todo.set_tag(self.tag, self.value, self.force_add, p_old_value)
if old_src != self.todo.source(): if old_src != self.todo.source():
self.todolist.set_dirty() self.todolist.dirty = True
def _set(self): def _set(self):
if len(self.current_values) > 1: if len(self.current_values) > 1 and not self.force_add:
answer = self._choose() answer = self._choose()
if answer == "all": if answer == "all":
...@@ -124,7 +124,7 @@ class TagCommand(Command): ...@@ -124,7 +124,7 @@ class TagCommand(Command):
self._print() self._print()
def execute(self): def execute(self):
if not super(TagCommand, self).execute(): if not super().execute():
return False return False
self._process_args() self._process_args()
...@@ -133,15 +133,15 @@ class TagCommand(Command): ...@@ -133,15 +133,15 @@ class TagCommand(Command):
self._set() self._set()
def usage(self): def usage(self):
return """Synopsis: tag [-a] [-f] <NUMBER> <tag> [<value>]""" return """Synopsis: tag [-a] [-f] <NUMBER> <TAG> [<VALUE>]"""
def help(self): def help(self):
return """\ return """\
Sets the given tag to the given todo number with the given value. If the value Sets the given TAG on the given todo NUMBER with the given VALUE. If the VALUE
is omitted, the tag is removed from the todo item. is omitted, the TAG is removed from the todo item.
-a : Do not change the current value of the tag if it exists, but add a new -a : Do not change the current value of the TAG if it exists, but add a new
value. VALUE for the given TAG.
-f : Force setting/removing all values of the tag. Prevents interaction with -f : Force setting/removing all values of the TAG. Prevents interaction with
the user. the user.\
""" """
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2016 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" This module provides a class that represents a color. """
class AbstractColor:
NEUTRAL = 0
PROJECT = 1
CONTEXT = 2
META = 3
LINK = 4
class Color:
color_names_dict = {
'black': 0,
'red': 1,
'green': 2,
'yellow': 3,
'blue': 4,
'magenta': 5,
'cyan': 6,
'gray': 7,
'darkgray': 8,
'light-red': 9,
'light-green': 10,
'light-yellow': 11,
'light-blue': 12,
'light-magenta': 13,
'light-cyan': 14,
'white': 15,
}
# Source: https://gist.github.com/jasonm23/2868981
html_color_dict = {
0: "#000000", 1: "#800000", 2: "#008000", 3: "#808000", 4: "#000080",
5: "#800080", 6: "#008080", 7: "#c0c0c0", 8: "#808080", 9: "#ff0000",
10: "#00ff00", 11: "#ffff00", 12: "#0000ff", 13: "#ff00ff", 14: "#00ffff",
15: "#ffffff", 16: "#000000", 17: "#00005f", 18: "#000087", 19: "#0000af",
20: "#0000d7", 21: "#0000ff", 22: "#005f00", 23: "#005f5f", 24: "#005f87",
25: "#005faf", 26: "#005fd7", 27: "#005fff", 28: "#008700", 29: "#00875f",
30: "#008787", 31: "#0087af", 32: "#0087d7", 33: "#0087ff", 34: "#00af00",
35: "#00af5f", 36: "#00af87", 37: "#00afaf", 38: "#00afd7", 39: "#00afff",
40: "#00d700", 41: "#00d75f", 42: "#00d787", 43: "#00d7af", 44: "#00d7d7",
45: "#00d7ff", 46: "#00ff00", 47: "#00ff5f", 48: "#00ff87", 49: "#00ffaf",
50: "#00ffd7", 51: "#00ffff", 52: "#5f0000", 53: "#5f005f", 54: "#5f0087",
55: "#5f00af", 56: "#5f00d7", 57: "#5f00ff", 58: "#5f5f00", 59: "#5f5f5f",
60: "#5f5f87", 61: "#5f5faf", 62: "#5f5fd7", 63: "#5f5fff", 64: "#5f8700",
65: "#5f875f", 66: "#5f8787", 67: "#5f87af", 68: "#5f87d7", 69: "#5f87ff",
70: "#5faf00", 71: "#5faf5f", 72: "#5faf87", 73: "#5fafaf", 74: "#5fafd7",
75: "#5fafff", 76: "#5fd700", 77: "#5fd75f", 78: "#5fd787", 79: "#5fd7af",
80: "#5fd7d7", 81: "#5fd7ff", 82: "#5fff00", 83: "#5fff5f", 84: "#5fff87",
85: "#5fffaf", 86: "#5fffd7", 87: "#5fffff", 88: "#870000", 89: "#87005f",
90: "#870087", 91: "#8700af", 92: "#8700d7", 93: "#8700ff", 94: "#875f00",
95: "#875f5f", 96: "#875f87", 97: "#875faf", 98: "#875fd7", 99: "#875fff",
100: "#878700", 101: "#87875f", 102: "#878787", 103: "#8787af", 104: "#8787d7",
105: "#8787ff", 106: "#87af00", 107: "#87af5f", 108: "#87af87", 109: "#87afaf",
110: "#87afd7", 111: "#87afff", 112: "#87d700", 113: "#87d75f", 114: "#87d787",
115: "#87d7af", 116: "#87d7d7", 117: "#87d7ff", 118: "#87ff00", 119: "#87ff5f",
120: "#87ff87", 121: "#87ffaf", 122: "#87ffd7", 123: "#87ffff", 124: "#af0000",
125: "#af005f", 126: "#af0087", 127: "#af00af", 128: "#af00d7", 129: "#af00ff",
130: "#af5f00", 131: "#af5f5f", 132: "#af5f87", 133: "#af5faf", 134: "#af5fd7",
135: "#af5fff", 136: "#af8700", 137: "#af875f", 138: "#af8787", 139: "#af87af",
140: "#af87d7", 141: "#af87ff", 142: "#afaf00", 143: "#afaf5f", 144: "#afaf87",
145: "#afafaf", 146: "#afafd7", 147: "#afafff", 148: "#afd700", 149: "#afd75f",
150: "#afd787", 151: "#afd7af", 152: "#afd7d7", 153: "#afd7ff", 154: "#afff00",
155: "#afff5f", 156: "#afff87", 157: "#afffaf", 158: "#afffd7", 159: "#afffff",
160: "#d70000", 161: "#d7005f", 162: "#d70087", 163: "#d700af", 164: "#d700d7",
165: "#d700ff", 166: "#d75f00", 167: "#d75f5f", 168: "#d75f87", 169: "#d75faf",
170: "#d75fd7", 171: "#d75fff", 172: "#d78700", 173: "#d7875f", 174: "#d78787",
175: "#d787af", 176: "#d787d7", 177: "#d787ff", 178: "#dfaf00", 179: "#dfaf5f",
180: "#dfaf87", 181: "#dfafaf", 182: "#dfafdf", 183: "#dfafff", 184: "#dfdf00",
185: "#dfdf5f", 186: "#dfdf87", 187: "#dfdfaf", 188: "#dfdfdf", 189: "#dfdfff",
190: "#dfff00", 191: "#dfff5f", 192: "#dfff87", 193: "#dfffaf", 194: "#dfffdf",
195: "#dfffff", 196: "#ff0000", 197: "#ff005f", 198: "#ff0087", 199: "#ff00af",
200: "#ff00df", 201: "#ff00ff", 202: "#ff5f00", 203: "#ff5f5f", 204: "#ff5f87",
205: "#ff5faf", 206: "#ff5fdf", 207: "#ff5fff", 208: "#ff8700", 209: "#ff875f",
210: "#ff8787", 211: "#ff87af", 212: "#ff87df", 213: "#ff87ff", 214: "#ffaf00",
215: "#ffaf5f", 216: "#ffaf87", 217: "#ffafaf", 218: "#ffafdf", 219: "#ffafff",
220: "#ffdf00", 221: "#ffdf5f", 222: "#ffdf87", 223: "#ffdfaf", 224: "#ffdfdf",
225: "#ffdfff", 226: "#ffff00", 227: "#ffff5f", 228: "#ffff87", 229: "#ffffaf",
230: "#ffffdf", 231: "#ffffff", 232: "#080808", 233: "#121212", 234: "#1c1c1c",
235: "#262626", 236: "#303030", 237: "#3a3a3a", 238: "#444444", 239: "#4e4e4e",
240: "#585858", 241: "#626262", 242: "#6c6c6c", 243: "#767676", 244: "#808080",
245: "#8a8a8a", 246: "#949494", 247: "#9e9e9e", 248: "#a8a8a8", 249: "#b2b2b2",
250: "#bcbcbc", 251: "#c6c6c6", 252: "#d0d0d0", 253: "#dadada", 254: "#e4e4e4",
255: "#eeeeee",
}
def __init__(self, p_value=None):
""" p_value is user input, be it a word color or an xterm code """
self._value = None
self.color = p_value
@property
def color(self):
return self._value
@color.setter
def color(self, p_value):
try:
if not p_value:
self._value = None
elif p_value in Color.color_names_dict:
self._value = Color.color_names_dict[p_value]
else:
self._value = int(p_value)
# values not in the 256 range are normalized to be neutral
if not 0 <= self._value < 256:
raise ValueError
except ValueError:
# garbage was entered, make it neutral, so at least some
# highlighting may take place
self._value = -1
def is_neutral(self):
"""
A neutral color is the default color on the shell, setting this color
will reset all other attributes (background, foreground, decoration).
"""
return self._value == -1
def is_valid(self):
"""
Whether the color is a valid color.
"""
return self._value is not None
def as_ansi(self, p_decoration='normal', p_background=False):
if not self.is_valid():
return ''
elif self.is_neutral():
return '\033[0m'
is_high_color = 8 <= self._value < 16
is_256 = 16 <= self._value < 255
decoration_dict = {
'normal': '0',
'bold': '1',
'faint': '2',
'italic': '3',
'underline': '4',
}
decoration = decoration_dict[p_decoration]
base = 40 if p_background else 30
if is_high_color:
color = '1;{}'.format(base + self._value - 8)
elif is_256:
color = '{};5;{}'.format(base + 8, self._value)
else:
# it's a low color
color = str(base + self._value)
return '\033[{};{}m'.format(
decoration,
color
)
def as_html(self):
try:
return Color.html_color_dict[self.color]
except KeyError:
return '#ffffff'
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" This module serves for managing output colors. """
from topydo.lib.Config import config
NEUTRAL_COLOR = '\033[0m'
def int_to_ansi(p_int, p_decorator='normal', p_safe=True, p_background=''):
"""
Returns ansi code for color based on xterm color id (0-255) and
decoration, where decoration can be one of: normal, bold, faint,
italic, or underline. When p_safe is True, resulting ansi code is
constructed in most compatible way, but with support for only base 16
colors.
"""
decoration_dict = {
'normal': '0',
'bold': '1',
'faint': '2',
'italic': '3',
'underline': '4'
}
decoration = decoration_dict[p_decorator]
try:
if p_safe:
if p_background:
p_background = ';4{}'.format(p_background)
if 8 > int(p_int) >= 0:
return '\033[{};3{}{}m'.format(decoration, str(p_int), p_background)
elif 16 > int(p_int):
p_int = int(p_int) - 8
return '\033[{};1;3{}{}m'.format(decoration, str(p_int), p_background)
if 256 > int(p_int) >= 0:
if p_background:
p_background = ';48;5;{}'.format(str(p_int))
return '\033[{};38;5;{}{}m'.format(decoration, str(p_int), p_background)
else:
return NEUTRAL_COLOR
except ValueError:
return None
def _name_to_int(p_color_name):
""" Returns xterm color id from color name. """
color_names_dict = {
'black': 0,
'red': 1,
'green': 2,
'yellow': 3,
'blue': 4,
'magenta': 5,
'cyan': 6,
'gray': 7,
'darkgray': 8,
'light-red': 9,
'light-green': 10,
'light-yellow': 11,
'light-blue': 12,
'light-magenta': 13,
'light-cyan': 14,
'white': 15,
}
try:
return color_names_dict[p_color_name]
except KeyError:
return 404
def _name_to_ansi(p_color_name, p_decorator):
""" Returns ansi color code from color name. """
number = _name_to_int(p_color_name)
return int_to_ansi(number, p_decorator)
def _get_ansi(p_color, p_decorator):
""" Returns ansi color code from color name or xterm color id. """
if p_color == '':
ansi = ''
else:
ansi = int_to_ansi(p_color, p_decorator, False)
if not ansi:
ansi = _name_to_ansi(p_color, p_decorator)
return ansi
def _get_priority_colors():
pri_ansi_colors = dict()
pri_colors = config().priority_colors()
for pri in pri_colors:
color = _get_ansi(pri_colors[pri], 'normal')
if color == '':
color = NEUTRAL_COLOR
pri_ansi_colors[pri] = color
return pri_ansi_colors
class Colors(object):
def __init__(self):
self.priority_colors = _get_priority_colors()
self.project_color = config().project_color()
self.context_color = config().context_color()
self.metadata_color = config().metadata_color()
self.link_color = config().link_color()
def get_project_color(self):
return _get_ansi(self.project_color, 'bold')
def get_context_color(self):
return _get_ansi(self.context_color, 'bold')
def get_metadata_color(self):
return _get_ansi(self.metadata_color, 'bold')
def get_link_color(self):
return _get_ansi(self.link_color, 'underline')
def get_priority_color(self, p_priority):
try:
priority_color = self.priority_colors[p_priority]
except KeyError:
priority_color = NEUTRAL_COLOR
return priority_color
...@@ -71,8 +71,8 @@ class Command(object): ...@@ -71,8 +71,8 @@ class Command(object):
""" Retrieves a value from the argument list at the given position. """ """ Retrieves a value from the argument list at the given position. """
try: try:
return self.args[p_number] return self.args[p_number]
except IndexError: except IndexError as ie:
raise InvalidCommandArgument raise InvalidCommandArgument from ie
def getopt(self, p_flags, p_long=None): def getopt(self, p_flags, p_long=None):
p_long = p_long or [] p_long = p_long or []
......
...@@ -16,8 +16,17 @@ ...@@ -16,8 +16,17 @@
import configparser import configparser
import os import os
import re
import shlex import shlex
from itertools import accumulate
from string import ascii_lowercase
from topydo.lib.Color import Color
def home_config_path(p_filename):
return os.path.join(os.path.expanduser('~'), p_filename)
class ConfigError(Exception): class ConfigError(Exception):
def __init__(self, p_text): def __init__(self, p_text):
self.text = p_text self.text = p_text
...@@ -43,6 +52,7 @@ class _Config: ...@@ -43,6 +52,7 @@ class _Config:
'add', 'add',
'aliases', 'aliases',
'colorscheme', 'colorscheme',
'columns',
'dep', 'dep',
'ls', 'ls',
'sort', 'sort',
...@@ -53,7 +63,8 @@ class _Config: ...@@ -53,7 +63,8 @@ class _Config:
self.defaults = { self.defaults = {
'topydo': { 'topydo': {
'default_command': 'ls', 'default_command': 'ls',
'colors': '1', 'colors': 'auto',
'force_colors': '0',
'filename': 'todo.txt', 'filename': 'todo.txt',
'archive_filename': 'done.txt', 'archive_filename': 'done.txt',
'identifiers': 'linenumber', 'identifiers': 'linenumber',
...@@ -106,11 +117,47 @@ class _Config: ...@@ -106,11 +117,47 @@ class _Config:
'listcontext': 'lscon', 'listcontext': 'lscon',
'listcontexts': 'lscon', 'listcontexts': 'lscon',
}, },
'columns': {
'column_width': '40',
},
'column_keymap': {
'gg': 'home',
'G': 'end',
'j': 'down',
'k': 'up',
'd': 'cmd del {}',
'e': 'cmd edit {}',
'u': 'cmd revert',
'x': 'cmd do {}',
'm': 'mark',
'.': 'repeat',
'pp': 'postpone',
'ps': 'postpone_s',
'pr': 'pri',
'0': 'first_column',
'$': 'last_column',
'h': 'prev_column',
'l': 'next_column',
'A': 'append_column',
'I': 'insert_column',
'E': 'edit_column',
'D': 'delete_column',
'Y': 'copy_column',
'L': 'swap_left',
'R': 'swap_right',
'<Left>': 'prev_column',
'<Right>': 'next_column',
'<Esc>': 'reset',
},
} }
self.config = {} self.config = {}
self.cp = configparser.RawConfigParser() self.cp = configparser.RawConfigParser()
# allow uppercase config keys
self.cp.optionxform = lambda option: option
for section in self.defaults: for section in self.defaults:
self.cp.add_section(section) self.cp.add_section(section)
...@@ -120,7 +167,8 @@ class _Config: ...@@ -120,7 +167,8 @@ class _Config:
files = [ files = [
"/etc/topydo.conf", "/etc/topydo.conf",
self._home_config_path(), home_config_path('.config/topydo/config'),
home_config_path('.topydo'),
".topydo", ".topydo",
"topydo.conf", "topydo.conf",
"topydo.ini", "topydo.ini",
...@@ -143,17 +191,44 @@ class _Config: ...@@ -143,17 +191,44 @@ class _Config:
if not self.cp.has_section(section): if not self.cp.has_section(section):
self.cp.add_section(section) self.cp.add_section(section)
def _home_config_path(self):
return os.path.join(os.path.expanduser('~'), '.topydo')
def default_command(self): def default_command(self):
return self.cp.get('topydo', 'default_command') return self.cp.get('topydo', 'default_command')
def colors(self): def colors(self, p_hint_possible=True):
"""
Returns 0, 16 or 256 representing the number of colors that should be
used in the output.
A hint can be passed whether the device that will output the text
supports colors.
"""
lookup = {
'false': 0,
'no': 0,
'0': 0,
'1': 16,
'true': 16,
'yes': 16,
'16': 16,
'256': 256,
}
try: try:
return self.cp.getboolean('topydo', 'colors') forced = self.cp.get('topydo', 'force_colors') == '1'
except ValueError: except ValueError:
return self.defaults['topydo']['colors'] == '1' forced = self.defaults['topydo']['force_colors'] == '1'
try:
colors = lookup[self.cp.get('topydo', 'colors').lower()] # pylint: disable=no-member
except ValueError:
colors = lookup[self.defaults['topydo']['colors'].lower()] # pylint: disable=no-member
except KeyError:
# for invalid values or 'auto'
colors = 16 if p_hint_possible else 0
# disable colors when no colors are enforced on the commandline and
# color support is determined automatically
return 0 if not forced and not p_hint_possible else colors
def todotxt(self): def todotxt(self):
return os.path.expanduser(self.cp.get('topydo', 'filename')) return os.path.expanduser(self.cp.get('topydo', 'filename'))
...@@ -236,53 +311,53 @@ class _Config: ...@@ -236,53 +311,53 @@ class _Config:
return [] if hidden_tags == '' else [tag.strip() for tag in return [] if hidden_tags == '' else [tag.strip() for tag in
hidden_tags.split(',')] hidden_tags.split(',')]
def priority_colors(self): def priority_color(self, p_priority):
""" """
Returns a dict with priorities as keys and color numbers as value. Returns a dict with priorities as keys and color numbers as value.
""" """
pri_colors_str = self.cp.get('colorscheme', 'priority_colors')
def _str_to_dict(p_string): def _str_to_dict(p_string):
pri_colors_dict = dict() pri_colors_dict = dict()
for pri_color in p_string.split(','): for pri_color in p_string.split(','):
pri, color = pri_color.split(':') pri, color = pri_color.split(':')
pri_colors_dict[pri] = color pri_colors_dict[pri] = Color(color)
return pri_colors_dict return pri_colors_dict
try: try:
pri_colors_str = self.cp.get('colorscheme', 'priority_colors')
if pri_colors_str == '': if pri_colors_str == '':
pri_colors_dict = {'A': '', 'B': '', 'C': ''} pri_colors_dict = _str_to_dict('A:-1,B:-1,C:-1')
else: else:
pri_colors_dict = _str_to_dict(pri_colors_str) pri_colors_dict = _str_to_dict(pri_colors_str)
except ValueError: except ValueError:
pri_colors_dict = _str_to_dict(self.defaults['colorscheme']['priority_colors']) pri_colors_dict = _str_to_dict(self.defaults['colorscheme']['priority_colors'])
return pri_colors_dict return pri_colors_dict[p_priority] if p_priority in pri_colors_dict else Color('NEUTRAL')
def project_color(self): def project_color(self):
try: try:
return self.cp.get('colorscheme', 'project_color') return Color(self.cp.getint('colorscheme', 'project_color'))
except ValueError: except ValueError:
return int(self.defaults['colorscheme']['project_color']) return Color(self.cp.get('colorscheme', 'project_color'))
def context_color(self): def context_color(self):
try: try:
return self.cp.get('colorscheme', 'context_color') return Color(self.cp.getint('colorscheme', 'context_color'))
except ValueError: except ValueError:
return int(self.defaults['colorscheme']['context_color']) return Color(self.cp.get('colorscheme', 'context_color'))
def metadata_color(self): def metadata_color(self):
try: try:
return self.cp.get('colorscheme', 'metadata_color') return Color(self.cp.getint('colorscheme', 'metadata_color'))
except ValueError: except ValueError:
return int(self.defaults['colorscheme']['metadata_color']) return Color(self.cp.get('colorscheme', 'metadata_color'))
def link_color(self): def link_color(self):
try: try:
return self.cp.get('colorscheme', 'link_color') return Color(self.cp.getint('colorscheme', 'link_color'))
except ValueError: except ValueError:
return int(self.defaults['colorscheme']['link_color']) return Color(self.cp.get('colorscheme', 'link_color'))
def auto_creation_date(self): def auto_creation_date(self):
try: try:
...@@ -299,10 +374,13 @@ class _Config: ...@@ -299,10 +374,13 @@ class _Config:
alias_dict = dict() alias_dict = dict()
for alias, meaning in aliases: for alias, meaning in aliases:
meaning = shlex.split(meaning) try:
real_subcommand = meaning[0] meaning = shlex.split(meaning)
alias_args = meaning[1:] real_subcommand = meaning[0]
alias_dict[alias] = (real_subcommand, alias_args) alias_args = meaning[1:]
alias_dict[alias] = (real_subcommand, alias_args)
except ValueError as verr:
alias_dict[alias] = str(verr)
return alias_dict return alias_dict
...@@ -310,6 +388,40 @@ class _Config: ...@@ -310,6 +388,40 @@ class _Config:
""" Returns the list format used by `ls` """ """ Returns the list format used by `ls` """
return self.cp.get('ls', 'list_format') return self.cp.get('ls', 'list_format')
def column_width(self):
try:
width = self.cp.getint('columns', 'column_width')
if width < 1:
# read default
raise ValueError
return width
except ValueError:
return int(self.defaults['columns']['column_width'])
def column_keymap(self):
""" Returns keymap and keystates used in column mode """
keystates = set()
shortcuts = self.cp.items('column_keymap')
keymap_dict = dict(shortcuts)
for combo, action in shortcuts:
# add all possible prefixes to keystates
combo_as_list = re.split('(<[A-Z].+?>|.)', combo)[1::2]
if len(combo_as_list) > 1:
keystates |= set(accumulate(combo_as_list[:-1]))
if action in ['pri', 'postpone', 'postpone_s']:
keystates.add(combo)
if action == 'pri':
for c in ascii_lowercase:
keymap_dict[combo + c] = 'cmd pri {} ' + c
return (keymap_dict, keystates)
def config(p_path=None, p_overrides=None): def config(p_path=None, p_overrides=None):
""" """
...@@ -327,7 +439,7 @@ def config(p_path=None, p_overrides=None): ...@@ -327,7 +439,7 @@ def config(p_path=None, p_overrides=None):
try: try:
config.instance = _Config(p_path, p_overrides) config.instance = _Config(p_path, p_overrides)
except configparser.ParsingError as perr: except configparser.ParsingError as perr:
raise ConfigError(str(perr)) raise ConfigError(str(perr)) from perr
return config.instance return config.instance
......
...@@ -31,7 +31,7 @@ class DCommand(MultiCommand): ...@@ -31,7 +31,7 @@ class DCommand(MultiCommand):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(DCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.force = False self.force = False
......
...@@ -32,7 +32,7 @@ class ExpressionCommand(Command): ...@@ -32,7 +32,7 @@ class ExpressionCommand(Command):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(ExpressionCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.sort_expression = config().sort_string() self.sort_expression = config().sort_string()
...@@ -45,38 +45,12 @@ class ExpressionCommand(Command): ...@@ -45,38 +45,12 @@ class ExpressionCommand(Command):
def _filters(self): def _filters(self):
filters = [] filters = []
def arg_filters():
result = []
if self.last_argument:
args = self.args[:-1]
else:
args = self.args
for arg in args:
# when a word starts with -, it should be negated
is_negated = len(arg) > 1 and arg[0] == '-'
arg = arg[1:] if is_negated else arg
if re.match(Filter.ORDINAL_TAG_MATCH, arg):
argfilter = Filter.OrdinalTagFilter(arg)
elif re.match(Filter.PRIORITY_MATCH, arg):
argfilter = Filter.PriorityFilter(arg)
else:
argfilter = Filter.GrepFilter(arg)
if is_negated:
argfilter = Filter.NegationFilter(argfilter)
result.append(argfilter)
return result
if not self.show_all: if not self.show_all:
filters.append(Filter.DependencyFilter(self.todolist)) filters.append(Filter.DependencyFilter(self.todolist))
filters.append(Filter.RelevanceFilter()) filters.append(Filter.RelevanceFilter())
filters += arg_filters() args = self.args[:-1] if self.last_argument else self.args
filters += Filter.get_filter_list(args)
if not self.show_all: if not self.show_all:
filters.append(Filter.LimitFilter(self.limit)) filters.append(Filter.LimitFilter(self.limit))
......
...@@ -62,7 +62,7 @@ class GrepFilter(Filter): ...@@ -62,7 +62,7 @@ class GrepFilter(Filter):
""" Matches when the todo text contains a text. """ """ Matches when the todo text contains a text. """
def __init__(self, p_expression, p_case_sensitive=None): def __init__(self, p_expression, p_case_sensitive=None):
super(GrepFilter, self).__init__() super().__init__()
# convert to string in case we receive integers # convert to string in case we receive integers
self.expression = p_expression self.expression = p_expression
...@@ -96,13 +96,15 @@ class RelevanceFilter(Filter): ...@@ -96,13 +96,15 @@ class RelevanceFilter(Filter):
""" """
def match(self, p_todo): def match(self, p_todo):
is_due = p_todo.is_active() active = 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
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): class DependencyFilter(Filter):
...@@ -115,7 +117,7 @@ class DependencyFilter(Filter): ...@@ -115,7 +117,7 @@ class DependencyFilter(Filter):
Pass on a TodoList instance such that the dependencies can be Pass on a TodoList instance such that the dependencies can be
looked up. looked up.
""" """
super(DependencyFilter, self).__init__() super().__init__()
self.todolist = p_todolist self.todolist = p_todolist
def match(self, p_todo): def match(self, p_todo):
...@@ -138,7 +140,7 @@ class InstanceFilter(Filter): ...@@ -138,7 +140,7 @@ class InstanceFilter(Filter):
This is handy for constructing a view given a plain list of Todo items. This is handy for constructing a view given a plain list of Todo items.
""" """
super(InstanceFilter, self).__init__() super().__init__()
self.todos = p_todos self.todos = p_todos
def match(self, p_todo): def match(self, p_todo):
...@@ -154,20 +156,20 @@ class InstanceFilter(Filter): ...@@ -154,20 +156,20 @@ class InstanceFilter(Filter):
class LimitFilter(Filter): class LimitFilter(Filter):
def __init__(self, p_limit): def __init__(self, p_limit):
super(LimitFilter, self).__init__() super().__init__()
self.limit = p_limit self.limit = p_limit
def filter(self, p_todos): def filter(self, p_todos):
return p_todos[:self.limit] if self.limit >= 0 else p_todos return p_todos[:self.limit] if self.limit >= 0 else p_todos
OPERATOR_MATCH = r"(?P<operator><=?|=|>=?|!)?" _OPERATOR_MATCH = r"(?P<operator><=?|=|>=?|!)?"
class OrdinalFilter(Filter): class OrdinalFilter(Filter):
""" Base class for ordinal filters. """ """ Base class for ordinal filters. """
def __init__(self, p_expression, p_pattern): def __init__(self, p_expression, p_pattern):
super(OrdinalFilter, self).__init__() super().__init__()
self.expression = p_expression self.expression = p_expression
...@@ -200,12 +202,13 @@ class OrdinalFilter(Filter): ...@@ -200,12 +202,13 @@ class OrdinalFilter(Filter):
return False return False
ORDINAL_TAG_MATCH = r"(?P<key>[^:]*):" + OPERATOR_MATCH + r"(?P<value>\S+)" _VALUE_MATCH = r"(?P<value>\S+)"
_ORDINAL_TAG_MATCH = r"(?P<key>[^:]*):" + _OPERATOR_MATCH + _VALUE_MATCH
class OrdinalTagFilter(OrdinalFilter): class OrdinalTagFilter(OrdinalFilter):
def __init__(self, p_expression): 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): def match(self, p_todo):
""" """
...@@ -243,12 +246,55 @@ class OrdinalTagFilter(OrdinalFilter): ...@@ -243,12 +246,55 @@ class OrdinalTagFilter(OrdinalFilter):
return self.compare_operands(operand1, operand2) return self.compare_operands(operand1, operand2)
PRIORITY_MATCH = r"\(" + OPERATOR_MATCH + r"(?P<value>[A-Z]{1})\)"
class _DateAttributeFilter(OrdinalFilter):
def __init__(self, p_expression, p_match, p_getter):
super().__init__(p_expression, p_match)
self.getter = p_getter
def match(self, p_todo):
operand1 = self.getter(p_todo)
operand2 = relative_date_to_date(self.value)
if not operand2:
operand2 = date_string_to_date(self.value)
if operand1 and operand2:
return self.compare_operands(operand1, operand2)
else:
return False
_CREATED_MATCH = r'creat(ion|ed?):' + _OPERATOR_MATCH + _VALUE_MATCH
class CreationFilter(_DateAttributeFilter):
def __init__(self, p_expression):
super().__init__(
p_expression,
_CREATED_MATCH,
lambda t: t.creation_date() # pragma: no branch
)
_COMPLETED_MATCH = r'complet(ed?|ion):' + _OPERATOR_MATCH + _VALUE_MATCH
class CompletionFilter(_DateAttributeFilter):
def __init__(self, p_expression):
super().__init__(
p_expression,
_COMPLETED_MATCH,
lambda t: t.completion_date() # pragma: no branch
)
_PRIORITY_MATCH = r"\(" + _OPERATOR_MATCH + r"(?P<value>[A-Z]{1})\)"
class PriorityFilter(OrdinalFilter): class PriorityFilter(OrdinalFilter):
def __init__(self, p_expression): def __init__(self, p_expression):
super(PriorityFilter, self).__init__(p_expression, PRIORITY_MATCH) super().__init__(p_expression, _PRIORITY_MATCH)
def match(self, p_todo): def match(self, p_todo):
""" """
...@@ -265,3 +311,39 @@ class PriorityFilter(OrdinalFilter): ...@@ -265,3 +311,39 @@ class PriorityFilter(OrdinalFilter):
operand2 = p_todo.priority() or 'ZZ' operand2 = p_todo.priority() or 'ZZ'
return self.compare_operands(operand1, operand2) return self.compare_operands(operand1, operand2)
MATCHES = [
(_CREATED_MATCH, CreationFilter),
(_COMPLETED_MATCH, CompletionFilter),
(_ORDINAL_TAG_MATCH, OrdinalTagFilter),
(_PRIORITY_MATCH, PriorityFilter),
]
def get_filter_list(p_expression):
"""
Returns a list of GrepFilters, OrdinalTagFilters or NegationFilters based
on the given filter expression.
The filter expression is a list of strings.
"""
result = []
for arg in p_expression:
# when a word starts with -, it should be negated
is_negated = len(arg) > 1 and arg[0] == '-'
arg = arg[1:] if is_negated else arg
argfilter = None
for match, _filter in MATCHES:
if re.match(match, arg):
argfilter = _filter(arg)
break
if not argfilter:
argfilter = GrepFilter(arg)
if is_negated:
argfilter = NegationFilter(argfilter)
result.append(argfilter)
return result
...@@ -83,12 +83,14 @@ def average_importance(p_todo, p_ignore_weekend=config().ignore_weekends()): ...@@ -83,12 +83,14 @@ def average_importance(p_todo, p_ignore_weekend=config().ignore_weekends()):
average = 0 average = 0
parents = [] parents = []
if 'parents' in p_todo.attributes: try:
sum_importance = own_importance sum_importance = own_importance
parents = p_todo.attributes['parents'] parents = p_todo.parents()
for parent in parents: for parent in parents:
sum_importance += importance(parent, p_ignore_weekend) sum_importance += importance(parent, p_ignore_weekend)
average = float(sum_importance) / float(1 + len(parents)) average = float(sum_importance) / float(1 + len(parents))
except AttributeError:
pass
return max(own_importance, average) return max(own_importance, average)
...@@ -20,7 +20,7 @@ import arrow ...@@ -20,7 +20,7 @@ import arrow
import re import re
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib.Colorblock import color_block from topydo.lib.ProgressColor import progress_color
from topydo.lib.Utils import get_terminal_size, escape_ansi, humanize_date from topydo.lib.Utils import get_terminal_size, escape_ansi, humanize_date
MAIN_PATTERN = (r'^({{(?P<before>.+?)}})?' MAIN_PATTERN = (r'^({{(?P<before>.+?)}})?'
...@@ -123,6 +123,12 @@ def _right_align(p_str): ...@@ -123,6 +123,12 @@ def _right_align(p_str):
return p_str return p_str
def color_block(p_todo):
return '{} {}'.format(
progress_color(p_todo).as_ansi(p_background=True),
config().priority_color(p_todo.priority()).as_ansi(),
)
class ListFormatParser(object): class ListFormatParser(object):
""" Parser of format string. """ """ Parser of format string. """
def __init__(self, p_todolist, p_format=None): def __init__(self, p_todolist, p_format=None):
...@@ -147,6 +153,7 @@ class ListFormatParser(object): ...@@ -147,6 +153,7 @@ class ListFormatParser(object):
# relative dates in form: creation, due, start # relative dates in form: creation, due, start
'H': lambda t: humanize_dates(t.due_date(), t.start_date(), t.creation_date()), 'H': lambda t: humanize_dates(t.due_date(), t.start_date(), t.creation_date()),
# todo ID # todo ID
'i': lambda t: str(self.todolist.number(t)), 'i': lambda t: str(self.todolist.number(t)),
...@@ -167,6 +174,12 @@ class ListFormatParser(object): ...@@ -167,6 +174,12 @@ class ListFormatParser(object):
# priority # priority
'p': lambda t: t.priority() if t.priority() else '', 'p': lambda t: t.priority() if t.priority() else '',
# priority (or placeholder space)
'P': lambda t: t.priority() if t.priority() else ' ',
# raw text
'r': lambda t: t.source(),
# text # text
's': lambda t: t.text(), 's': lambda t: t.text(),
...@@ -186,8 +199,6 @@ class ListFormatParser(object): ...@@ -186,8 +199,6 @@ class ListFormatParser(object):
'X': lambda t: 'x ' + humanize_date(t.completion_date()) if t.is_completed() else '', 'X': lambda t: 'x ' + humanize_date(t.completion_date()) if t.is_completed() else '',
'z': lambda t: color_block(t) if config().colors() else ' ', 'z': lambda t: color_block(t) if config().colors() else ' ',
'Z': lambda t: color_block(t, p_safe=False) if config().colors() else ' ',
} }
self.format_list = self._preprocess_format() self.format_list = self._preprocess_format()
......
...@@ -27,7 +27,7 @@ class MultiCommand(ExpressionCommand): ...@@ -27,7 +27,7 @@ class MultiCommand(ExpressionCommand):
p_out=lambda a: None, p_out=lambda a: None,
p_err=lambda a: None, p_err=lambda a: None,
p_prompt=lambda a: None): p_prompt=lambda a: None):
super(MultiCommand, self).__init__( super().__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.todos = [] self.todos = []
...@@ -114,7 +114,7 @@ class MultiCommand(ExpressionCommand): ...@@ -114,7 +114,7 @@ class MultiCommand(ExpressionCommand):
raise NotImplementedError raise NotImplementedError
def execute(self): def execute(self):
if not super(MultiCommand, self).execute(): if not super().execute():
return False return False
self._process_flags() self._process_flags()
......
...@@ -29,4 +29,3 @@ class PrettyPrinterFilter(object): ...@@ -29,4 +29,3 @@ class PrettyPrinterFilter(object):
Applies a filter to p_todo_str and returns a modified version of it. Applies a filter to p_todo_str and returns a modified version of it.
""" """
raise NotImplementedError raise NotImplementedError
# Topydo - A todo.txt client written in Python. # Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl> # Copyright (C) 2016 Bram Schoenmakers <me@bramschoenmakers.nl>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
...@@ -16,106 +16,91 @@ ...@@ -16,106 +16,91 @@
import re import re
from topydo.lib.Colors import int_to_ansi, Colors from topydo.lib.Color import Color
from topydo.lib.Config import config
from topydo.lib.Recurrence import relative_date_to_date from topydo.lib.Recurrence import relative_date_to_date
COLOR16_RANGE = [
(10, '#00ff00', '#000000'), # light green
(2, '#008700', '#ffffff'), # green
(3, '#ffff00', '#000000'), # yellow
(1, '#ff0000', '#ffffff'), # red
]
# https://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg
# a gradient from green to yellow to red
COLOR256_RANGE = [
(22, '#005f00', '#ffffff'),
(28, '#008700', '#ffffff'),
(34, '#00af00', '#ffffff'),
(40, '#00d700', '#000000'),
(46, '#00ff00', '#000000'),
(82, '#5fff00', '#000000'),
(118, '#87ff00', '#000000'),
(154, '#afff00', '#000000'),
(190, '#dfff00', '#000000'),
(226, '#ffff00', '#000000'),
(220, '#ffd700', '#000000'),
(214, '#ffaf00', '#000000'),
(208, '#ff8700', '#000000'),
(202, '#ff5f00', '#ffffff'),
(196, '#ff0000', '#ffffff'),
]
def _progress_to_color(p_todo, p_safe=True):
def get_progress():
"""
Returns a value from 0 to 1 where we are today in a date range. Returns
a value >1 when a todo item is overdue.
"""
def get_length(): # when a todo item has not enough information to determine the length, assume
""" # this length
Returns the length of the p_todo item in days, based on the recurrence ASSUMED_TODO_LENGTH = 14 # days
period + due date, or the start/due date.
"""
result = 0
def diff_days(p_start, p_end): def progress_color(p_todo):
if p_start < p_end: color16_range = [
diff = p_end - p_start 2, # green
return diff.days 10, # light green
3, # yellow
1, # red
]
return 0 # https://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg
# a gradient from green to yellow to red
color256_range = \
[22, 28, 34, 40, 46, 82, 118, 154, 190, 226, 220, 214, 208, 202, 196]
if p_todo.has_tag('rec') and p_todo.due_date(): def get_length():
# add negation, offset is based on due date """
recurrence_pattern = p_todo.tag_value('rec') Returns the length of the p_todo item in days, based on the recurrence
neg_recurrence_pattern = re.sub('^\+?', '-', recurrence_pattern) period + due date, or the start/due date.
"""
result = 0
start = relative_date_to_date( def diff_days(p_start, p_end):
neg_recurrence_pattern, p_todo.due_date()) """
due = p_todo.due_date() Returns the difference in days between p_start and p_end, where
start is before due.
"""
diff = p_end - p_start
return diff.days
does_recur = p_todo.has_tag('rec')
start_date = p_todo.start_date()
due_date = p_todo.due_date()
creation_date = p_todo.creation_date()
if does_recur and due_date and not start_date:
# add negation, offset is based on due date
recurrence_pattern = p_todo.tag_value('rec')
neg_recurrence_pattern = re.sub('^\+?', '-', recurrence_pattern)
start = relative_date_to_date(neg_recurrence_pattern, due_date)
result = diff_days(start, due_date)
elif due_date and not start_date and not creation_date:
result = ASSUMED_TODO_LENGTH
elif due_date and start_date and due_date < start_date:
result = ASSUMED_TODO_LENGTH
elif due_date and not start_date and creation_date and due_date < creation_date:
result = ASSUMED_TODO_LENGTH
else:
result = p_todo.length()
result = diff_days(start, due) # a todo item is at least one day long
else: return max(1, result)
result = p_todo.length()
return result def get_progress():
"""
Returns a value from 0 to 1 where we are today in a date range. Returns
a value >1 when a todo item is overdue.
"""
if p_todo.is_overdue(): if p_todo.is_overdue():
return 1.1 return 1.1
elif p_todo.due_date(): elif p_todo.due_date():
days_till_due = p_todo.days_till_due() days_till_due = p_todo.days_till_due()
length = get_length() or 14 length = get_length()
return max((length - days_till_due), 0) / length return max((length - days_till_due), 0) / length
else: else:
return 0 return 0
color_range = COLOR16_RANGE if p_safe else COLOR256_RANGE color_range = color256_range if config().colors() == 256 else color16_range
progress = get_progress() progress = get_progress()
# TODO: remove linear scale to exponential scale # TODO: remove linear scale to exponential scale
if progress > 1: if progress > 1:
# overdue, return the last color # overdue, return the last color
return color_range[-1] return Color(color_range[-1])
else: else:
# not overdue, calculate position over color range excl. due date # not overdue, calculate position over color range excl. due date
# color # color
pos = round(progress * (len(color_range) - 2)) pos = round(progress * (len(color_range) - 2))
return color_range[pos] return Color(color_range[pos])
def progress_color_code(p_todo, p_safe=True):
return _progress_to_color(p_todo, p_safe)[0]
def progress_html_color(p_todo):
""" Returns a tuple (foreground, background) color """
_, background, foreground = _progress_to_color(p_todo, p_safe=False)
return (foreground, background)
def color_block(p_todo, p_safe=True):
color_code = progress_color_code(p_todo, p_safe)
ansi_code = int_to_ansi(color_code, p_safe=p_safe, p_background=color_code)
priority_color = Colors().get_priority_color(p_todo.priority())
return '{} {}'.format(ansi_code, priority_color)
...@@ -37,9 +37,26 @@ def _add_months(p_sourcedate, p_months): ...@@ -37,9 +37,26 @@ def _add_months(p_sourcedate, p_months):
return date(year, month, day) 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): 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. offset with the period of time added to it.
""" """
result = None result = None
...@@ -55,6 +72,8 @@ def _convert_pattern(p_length, p_periodunit, p_offset=None): ...@@ -55,6 +72,8 @@ def _convert_pattern(p_length, p_periodunit, p_offset=None):
result = _add_months(p_offset, p_length) result = _add_months(p_offset, p_length)
elif p_periodunit == 'y': elif p_periodunit == 'y':
result = _add_months(p_offset, p_length * 12) result = _add_months(p_offset, p_length * 12)
elif p_periodunit == 'b':
result = _add_business_days(p_offset, p_length)
return result return result
...@@ -98,7 +117,8 @@ def relative_date_to_date(p_date, p_offset=None): ...@@ -98,7 +117,8 @@ def relative_date_to_date(p_date, p_offset=None):
p_date = p_date.lower() p_date = p_date.lower()
p_offset = p_offset or date.today() 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)?)?$' monday = 'mo(n(day)?)?$'
tuesday = 'tu(e(sday)?)?$' tuesday = 'tu(e(sday)?)?$'
......
...@@ -52,6 +52,8 @@ def get_field_function(p_field): ...@@ -52,6 +52,8 @@ def get_field_function(p_field):
result = importance result = importance
elif p_field == 'importance-avg' or p_field == 'importance-average': elif p_field == 'importance-avg' or p_field == 'importance-average':
result = average_importance result = average_importance
elif p_field == 'length':
result = lambda a: a.length()
elif p_field == 'project' or p_field == 'projects': elif p_field == 'project' or p_field == 'projects':
result = lambda a: sorted([c.lower() for c in a.projects()]) result = lambda a: sorted([c.lower() for c in a.projects()])
elif p_field == 'text': elif p_field == 'text':
......
...@@ -48,17 +48,14 @@ class TodoBase(object): ...@@ -48,17 +48,14 @@ class TodoBase(object):
Returns a tag value associated with p_key. Returns p_default if p_key Returns a tag value associated with p_key. Returns p_default if p_key
does not exist (which defaults to None). does not exist (which defaults to None).
""" """
values = self.tag_values(p_key) return self.tag_values(p_key)[0] if p_key in self.fields['tags'] else p_default
return values[0] if len(values) else p_default
def tag_values(self, p_key): def tag_values(self, p_key):
""" """
Returns a list of all tag values associated with p_key. Returns Returns a list of all tag values associated with p_key. Returns
empty list if p_key does not exist. empty list if p_key does not exist.
""" """
tags = self.fields['tags'] return self.fields['tags'][p_key] if p_key in self.fields['tags'] else []
matches = [tag[1] for tag in tags if tag[0] == p_key]
return matches if len(matches) else []
def has_tag(self, p_key, p_value=""): def has_tag(self, p_key, p_value=""):
""" """
...@@ -66,14 +63,28 @@ class TodoBase(object): ...@@ -66,14 +63,28 @@ class TodoBase(object):
value is passed, it will only return true when there exists a tag with value is passed, it will only return true when there exists a tag with
the given key-value combination. the given key-value combination.
""" """
result = [t for t in self.tag_values(p_key) tags = self.fields['tags']
if p_value == "" or t == p_value] return p_key in tags and (p_value == "" or p_value in tags[p_key])
return len(result) > 0
def add_tag(self, p_key, p_value): def add_tag(self, p_key, p_value):
""" Adds a tag to the todo. """ """ Adds a tag to the todo. """
self.set_tag(p_key, p_value, True) 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=""): 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 Sets a occurrence of the tag identified by p_key. Sets an arbitrary
...@@ -92,12 +103,11 @@ class TodoBase(object): ...@@ -92,12 +103,11 @@ class TodoBase(object):
self.remove_tag(p_key, p_old_value) self.remove_tag(p_key, p_old_value)
return return
tags = self.fields['tags']
value = p_old_value if p_old_value else self.tag_value(p_key) value = p_old_value if p_old_value else self.tag_value(p_key)
if not p_force_add and value: if not p_force_add and value:
# remove old value from the tags self._remove_tag_helper(p_key, value)
self.fields['tags'] = [t for t in self.fields['tags']
if not (t[0] == p_key and t[1] == value)]
self.src = re.sub( self.src = re.sub(
r'\b' + p_key + ':' + value + r'\b', r'\b' + p_key + ':' + value + r'\b',
...@@ -107,7 +117,10 @@ class TodoBase(object): ...@@ -107,7 +117,10 @@ class TodoBase(object):
else: else:
self.src += ' ' + p_key + ':' + p_value 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=""): def remove_tag(self, p_key, p_value=""):
""" """
...@@ -116,12 +129,7 @@ class TodoBase(object): ...@@ -116,12 +129,7 @@ class TodoBase(object):
removed. removed.
Else, only those tags with the value will be removed. Else, only those tags with the value will be removed.
""" """
self._remove_tag_helper(p_key, p_value)
# 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))]
# when value == "", match any value having key p_key # when value == "", match any value having key p_key
value = p_value if p_value != "" else r'\S+' value = p_value if p_value != "" else r'\S+'
...@@ -132,7 +140,8 @@ class TodoBase(object): ...@@ -132,7 +140,8 @@ class TodoBase(object):
Returns a list of tuples with key-value pairs representing tags in Returns a list of tuples with key-value pairs representing tags in
this todo item. 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): def set_priority(self, p_priority):
""" """
......
...@@ -18,11 +18,35 @@ ...@@ -18,11 +18,35 @@
A list of todo items. A list of todo items.
""" """
import types
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib.Graph import DirectedGraph
from topydo.lib.TodoListBase import TodoListBase 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): class TodoList(TodoListBase):
""" """
Provides operations for a todo list, such as adding items, removing them, Provides operations for a todo list, such as adding items, removing them,
...@@ -37,21 +61,27 @@ class TodoList(TodoListBase): ...@@ -37,21 +61,27 @@ class TodoList(TodoListBase):
Should be given a list of strings, each element a single todo string. Should be given a list of strings, each element a single todo string.
The string will be parsed. The string will be parsed.
""" """
self._initialized = False # whether dependency information was
# initialized
# initialize these first because the constructor calls add_list # initialize these first because the constructor calls add_list
self._tododict = {} # hash(todo) to todo lookup 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): def todo_by_dep_id(self, p_dep_id):
""" """
Returns the todo that has the id tag set to the value 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 There is only one such task, the behavior is undefined when a todo item
more than one id tag. has more than one id tag.
""" """
hits = [t for t in self._todos if t.tag_value('id') == p_dep_id] try:
return self._parentdict[p_dep_id]
return hits[0] if len(hits) else None except KeyError:
return None
def _maintain_dep_graph(self, p_todo): def _maintain_dep_graph(self, p_todo):
""" """
...@@ -61,6 +91,7 @@ class TodoList(TodoListBase): ...@@ -61,6 +91,7 @@ class TodoList(TodoListBase):
dep_id = p_todo.tag_value('id') dep_id = p_todo.tag_value('id')
# maintain dependency graph # maintain dependency graph
if dep_id: if dep_id:
self._parentdict[dep_id] = p_todo
self._depgraph.add_node(hash(p_todo)) self._depgraph.add_node(hash(p_todo))
# connect all tasks we have in memory so far that refer to this # connect all tasks we have in memory so far that refer to this
...@@ -68,33 +99,43 @@ class TodoList(TodoListBase): ...@@ -68,33 +99,43 @@ class TodoList(TodoListBase):
for dep in \ for dep in \
[dep for dep in self._todos if dep.has_tag('p', dep_id)]: [dep for dep in self._todos if dep.has_tag('p', dep_id)]:
self._depgraph.add_edge(hash(p_todo), hash(dep), dep_id) self._add_edge(p_todo, dep, dep_id)
for dep_id in p_todo.tag_values('p'):
try:
parent = self._parentdict[dep_id]
self._add_edge(parent, p_todo, dep_id)
except KeyError:
pass
for child in p_todo.tag_values('p'): def _register_todo(self, p_todo):
parent = self.todo_by_dep_id(child) self._maintain_dep_graph(p_todo)
if parent: self._tododict[hash(p_todo)] = p_todo
self._depgraph.add_edge(hash(parent), hash(p_todo), child)
def add_todos(self, p_todos): def add_todos(self, p_todos):
for todo in p_todos: super().add_todos(p_todos)
self._todos.append(todo)
self._tododict[hash(todo)] = todo
self._maintain_dep_graph(todo)
self._update_todo_ids() for todo in self._todos:
self._update_parent_cache() todo.parents = types.MethodType(lambda i: self.parents(i), todo)
self.dirty = True
# 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): def delete(self, p_todo, p_leave_tags=False):
""" Deletes a todo item from the list. """ """ Deletes a todo item from the list. """
try: try:
number = self._todos.index(p_todo) number = self._todos.index(p_todo)
for child in self.children(p_todo): if p_todo.has_tag('id'):
self.remove_dependency(p_todo, child) for child in self.children(p_todo):
self.remove_dependency(p_todo, child, p_leave_tags)
for parent in self.parents(p_todo): if p_todo.has_tag('p'):
self.remove_dependency(parent, p_todo) for parent in self.parents(p_todo):
self.remove_dependency(parent, p_todo, p_leave_tags)
del self._todos[number] del self._todos[number]
self._update_todo_ids() self._update_todo_ids()
...@@ -104,6 +145,11 @@ class TodoList(TodoListBase): ...@@ -104,6 +145,11 @@ class TodoList(TodoListBase):
# todo item couldn't be found, ignore # todo item couldn't be found, ignore
pass pass
def _add_edge(self, p_from_todo, p_to_todo, p_dep_id):
self._parentdict[p_dep_id] = p_from_todo
self._depgraph.add_edge(hash(p_from_todo), hash(p_to_todo), p_dep_id)
@_needs_dependencies
def add_dependency(self, p_from_todo, p_to_todo): def add_dependency(self, p_from_todo, p_to_todo):
""" Adds a dependency from task 1 to task 2. """ """ Adds a dependency from task 1 to task 2. """
def find_next_id(): def find_next_id():
...@@ -117,7 +163,8 @@ class TodoList(TodoListBase): ...@@ -117,7 +163,8 @@ class TodoList(TodoListBase):
Returns True if there exists a todo with the given parent ID. Returns True if there exists a todo with the given parent ID.
""" """
for todo in self._todos: 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 True
return False return False
...@@ -157,26 +204,29 @@ class TodoList(TodoListBase): ...@@ -157,26 +204,29 @@ class TodoList(TodoListBase):
p_from_todo.set_tag('id', dep_id) p_from_todo.set_tag('id', dep_id)
p_to_todo.add_tag('p', dep_id) p_to_todo.add_tag('p', dep_id)
self._depgraph.add_edge(hash(p_from_todo), hash(p_to_todo), dep_id) self._add_edge(p_from_todo, p_to_todo, dep_id)
self._update_parent_cache()
append_projects_to_subtodo() append_projects_to_subtodo()
append_contexts_to_subtodo() append_contexts_to_subtodo()
self.dirty = True self.dirty = True
def remove_dependency(self, p_from_todo, p_to_todo): @_needs_dependencies
def remove_dependency(self, p_from_todo, p_to_todo, p_leave_tags=False):
""" Removes a dependency between two todos. """ """ Removes a dependency between two todos. """
dep_id = p_from_todo.tag_value('id') dep_id = p_from_todo.tag_value('id')
if dep_id: if dep_id:
p_to_todo.remove_tag('p', dep_id)
self._depgraph.remove_edge(hash(p_from_todo), hash(p_to_todo)) self._depgraph.remove_edge(hash(p_from_todo), hash(p_to_todo))
self._update_parent_cache() self.dirty = True
# clean dangling dependency tags
if dep_id and not p_leave_tags:
p_to_todo.remove_tag('p', dep_id)
if not self.children(p_from_todo, True): if not self.children(p_from_todo, True):
p_from_todo.remove_tag('id') 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): def parents(self, p_todo, p_only_direct=False):
""" """
Returns a list of parent todos that (in)directly depend on the Returns a list of parent todos that (in)directly depend on the
...@@ -186,6 +236,7 @@ class TodoList(TodoListBase): ...@@ -186,6 +236,7 @@ class TodoList(TodoListBase):
hash(p_todo), not p_only_direct) hash(p_todo), not p_only_direct)
return [self._tododict[parent] for parent in parents] return [self._tododict[parent] for parent in parents]
@_needs_dependencies
def children(self, p_todo, p_only_direct=False): def children(self, p_todo, p_only_direct=False):
""" """
Returns a list of child todos that the given todo (in)directly depends Returns a list of child todos that the given todo (in)directly depends
...@@ -195,6 +246,7 @@ class TodoList(TodoListBase): ...@@ -195,6 +246,7 @@ class TodoList(TodoListBase):
self._depgraph.outgoing_neighbors(hash(p_todo), not p_only_direct) self._depgraph.outgoing_neighbors(hash(p_todo), not p_only_direct)
return [self._tododict[child] for child in children] return [self._tododict[child] for child in children]
@_needs_dependencies
def clean_dependencies(self): def clean_dependencies(self):
""" """
Cleans the dependency graph. Cleans the dependency graph.
...@@ -219,6 +271,7 @@ class TodoList(TodoListBase): ...@@ -219,6 +271,7 @@ class TodoList(TodoListBase):
value = todo.tag_value('id') value = todo.tag_value('id')
if not self._depgraph.has_edge_id(value): if not self._depgraph.has_edge_id(value):
remove_tag(todo, 'id', value) remove_tag(todo, 'id', value)
del self._parentdict[value]
def clean_orphan_relations(): def clean_orphan_relations():
""" """
...@@ -237,12 +290,3 @@ class TodoList(TodoListBase): ...@@ -237,12 +290,3 @@ class TodoList(TodoListBase):
clean_parent_relations() clean_parent_relations()
clean_orphan_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)
...@@ -52,7 +52,13 @@ class TodoListBase(object): ...@@ -52,7 +52,13 @@ class TodoListBase(object):
self._id_todo_map = {} self._id_todo_map = {}
self.add_list(p_todostrings) self.add_list(p_todostrings)
self.dirty = False self._dirty = False
def __iter__(self):
"""
Allows use of `for my_todo in todolist` constructs.
"""
return iter(self._todos)
def todo(self, p_identifier): def todo(self, p_identifier):
""" """
...@@ -95,11 +101,11 @@ class TodoListBase(object): ...@@ -95,11 +101,11 @@ class TodoListBase(object):
# the expression is a string and no leading zeroes, # the expression is a string and no leading zeroes,
# treat it as an integer # treat it as an integer
raise TypeError raise TypeError
except TypeError: except TypeError as te:
try: try:
result = self._todos[int(p_identifier) - 1] result = self._todos[int(p_identifier) - 1]
except IndexError: except (ValueError, IndexError):
raise InvalidTodoException raise InvalidTodoException from te
return result return result
...@@ -221,11 +227,13 @@ class TodoListBase(object): ...@@ -221,11 +227,13 @@ class TodoListBase(object):
""" """
return View(p_sorter, p_filters, self) return View(p_sorter, p_filters, self)
def is_dirty(self): @property
return self.dirty def dirty(self):
return self._dirty
def set_dirty(self): @dirty.setter
self.dirty = True def dirty(self, p_flag):
self._dirty = p_flag
def todos(self): def todos(self):
return self._todos return self._todos
...@@ -249,8 +257,8 @@ class TodoListBase(object): ...@@ -249,8 +257,8 @@ class TodoListBase(object):
return self._todo_id_map[p_todo] return self._todo_id_map[p_todo]
else: else:
return self._todos.index(p_todo) + 1 return self._todos.index(p_todo) + 1
except (ValueError, KeyError): except (ValueError, KeyError) as ex:
raise InvalidTodoException raise InvalidTodoException from ex
def _update_todo_ids(self): def _update_todo_ids(self):
# the idea is to have a hash that is independent of the position of the # the idea is to have a hash that is independent of the position of the
...@@ -271,4 +279,4 @@ class TodoListBase(object): ...@@ -271,4 +279,4 @@ class TodoListBase(object):
this list. this list.
""" """
printer = PrettyPrinter() printer = PrettyPrinter()
return printer.print_list(self._todos) return "\n".join([str(s) for s in printer.print_list(self._todos)])
...@@ -33,7 +33,7 @@ _NORMAL_HEAD_MATCH = re.compile( ...@@ -33,7 +33,7 @@ _NORMAL_HEAD_MATCH = re.compile(
r'(\((?P<priority>[A-Z])\) )?' + '((?P<creationDate>' + _DATE_MATCH + r'(\((?P<priority>[A-Z])\) )?' + '((?P<creationDate>' + _DATE_MATCH +
') )?(?P<rest>.*)') ') )?(?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)') _PROJECT_MATCH = re.compile(r'\+(\S*\w)')
_CONTEXT_MATCH = re.compile(r'@(\S*\w)') _CONTEXT_MATCH = re.compile(r'@(\S*\w)')
...@@ -57,7 +57,7 @@ def parse_line(p_string): ...@@ -57,7 +57,7 @@ def parse_line(p_string):
'text': "", 'text': "",
'projects': [], 'projects': [],
'contexts': [], 'contexts': [],
'tags': [] 'tags': {},
} }
completed_head = _COMPLETED_HEAD_MATCH.match(p_string) completed_head = _COMPLETED_HEAD_MATCH.match(p_string)
...@@ -94,10 +94,14 @@ def parse_line(p_string): ...@@ -94,10 +94,14 @@ def parse_line(p_string):
tag = _TAG_MATCH.match(word) tag = _TAG_MATCH.match(word)
if tag: if tag:
result['tags'].append((tag.group('key'), tag.group('value'))) tag_name = tag.group('tag')
continue tag_value = tag.group('value')
try:
result['text'] += word + ' ' 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 # strip trailing space from resulting text
result['text'] = result['text'][:-1] result['text'] = result['text'][:-1]
......
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2016 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" This module provides TopydoString to embed colors in a string. """
import collections
class TopydoString(collections.UserString):
"""
Represents a string that also contains color information. A combination of
(position, color) is maintained, where the position is the start position
where a certain color should start.
"""
def __init__(self, p_content, p_metadata=None):
if isinstance(p_content, TopydoString):
# don't nest topydostrings
self.colors = p_content.colors
self.metadata = p_content.metadata
super().__init__(p_content.data)
else:
self.colors = {}
super().__init__(p_content)
# allows clients to pass arbitrary data with this string (e.g. a Todo
# object)
self.metadata = p_metadata
def append(self, p_string, p_color):
"""
Append a string with the given color (normal Color or an
AbstractColor).
"""
self.colors[len(self.data)] = p_color
self.data += p_string
def set_color(self, p_pos, p_color):
""" Start using a color at the given position. """
self.colors[p_pos] = p_color
...@@ -55,28 +55,65 @@ def escape_ansi(p_string): ...@@ -55,28 +55,65 @@ def escape_ansi(p_string):
escape_ansi.pattern = re.compile(r'\x1b[^m]*m') escape_ansi.pattern = re.compile(r'\x1b[^m]*m')
def get_terminal_size():
def get_terminal_size(p_getter=None):
""" """
Try to determine terminal size at run time. If that is not possible, Try to determine terminal size at run time. If that is not possible,
returns the default size of 80x24. returns the default size of 80x24.
By default, the size is determined with provided get_terminal_size by
shutil. Sometimes an UI may want to specify the desired width, then it can
provide a getter that returns a named tuple (columns, lines) with the size.
""" """
from shutil import get_terminal_size # pylint: disable=no-name-in-module
try: try:
sz = get_terminal_size() return get_terminal_size.getter()
except ValueError: except AttributeError:
""" if p_getter:
This can result from the 'underlying buffer being detached', which get_terminal_size.getter = p_getter
occurs during running the unittest on Windows (but not on Linux?) else:
""" def inner():
terminal_size = namedtuple('Terminal_Size', 'columns lines') try:
sz = terminal_size((80, 24)) # shutil.get_terminal_size was added to the standard
# library in Python 3.3
try:
from shutil import get_terminal_size as _get_terminal_size # pylint: disable=no-name-in-module
except ImportError:
from backports.shutil_get_terminal_size import get_terminal_size as _get_terminal_size # pylint: disable=import-error
sz = _get_terminal_size()
except ValueError:
"""
This can result from the 'underlying buffer being detached', which
occurs during running the unittest on Windows (but not on Linux?)
"""
terminal_size = namedtuple('Terminal_Size', 'columns lines')
sz = terminal_size(80, 24)
return sz
get_terminal_size.getter = inner
return get_terminal_size.getter()
def translate_key_to_config(p_key):
"""
Translates urwid key event to form understandable by topydo config parser.
"""
if len(p_key) > 1:
key = p_key.capitalize()
if key.startswith('Ctrl') or key.startswith('Meta'):
key = key[0] + '-' + key[5:]
key = '<' + key + '>'
else:
key = p_key
return sz return key
def humanize_date(p_datetime): def humanize_date(p_datetime):
""" Returns a relative date string from a datetime object. """ """ Returns a relative date string from a datetime object. """
now = arrow.now() now = arrow.now()
date = now.replace(day=p_datetime.day, month=p_datetime.month, year=p_datetime.year) 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')
""" Version of Topydo. """ """ Version of Topydo. """
VERSION = '0.8' VERSION = '0.9'
LICENSE = """Copyright (C) 2014-2015 Bram Schoenmakers LICENSE = """Copyright (C) 2014-2015 Bram Schoenmakers
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
......
...@@ -25,14 +25,14 @@ class View(object): ...@@ -25,14 +25,14 @@ class View(object):
""" """
def __init__(self, p_sorter, p_filters, p_todolist): def __init__(self, p_sorter, p_filters, p_todolist):
self._todolist = p_todolist self.todolist = p_todolist
self._sorter = p_sorter self._sorter = p_sorter
self._filters = p_filters self._filters = p_filters
@property @property
def todos(self): def todos(self):
""" Returns a sorted and filtered list of todos in this view. """ """ Returns a sorted and filtered list of todos in this view. """
result = self._sorter.sort(self._todolist.todos()) result = self._sorter.sort(self.todolist.todos())
for _filter in self._filters: for _filter in self._filters:
result = _filter.filter(result) result = _filter.filter(result)
......
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.Command import Command
from topydo.lib.Config import config
from topydo.lib.RelativeDate import relative_date_to_date
from topydo.lib.TodoListBase import InvalidTodoException
class WriteCommand(Command):
def postprocess_input_todo(self, p_todo):
"""
Post-processes a parsed todo when adding it to the list.
* It converts relative dates to absolute ones.
* Automatically inserts a creation date if not present.
* Handles more user-friendly dependencies with before:, partof: and
after: tags
"""
def convert_date(p_tag):
value = p_todo.tag_value(p_tag)
if value:
dateobj = relative_date_to_date(value)
if dateobj:
p_todo.set_tag(p_tag, dateobj.isoformat())
def add_dependencies(p_tag):
for value in p_todo.tag_values(p_tag):
try:
dep = self.todolist.todo(value)
if p_tag == 'after':
self.todolist.add_dependency(p_todo, dep)
elif p_tag == 'before' or p_tag == 'partof':
self.todolist.add_dependency(dep, p_todo)
elif p_tag.startswith('parent'):
for parent in self.todolist.parents(dep):
self.todolist.add_dependency(parent, p_todo)
elif p_tag.startswith('child'):
for child in self.todolist.children(dep):
self.todolist.add_dependency(p_todo, child)
except InvalidTodoException:
pass
p_todo.remove_tag(p_tag, value)
convert_date(config().tag_start())
convert_date(config().tag_due())
keywords = [
'after',
'before',
'child-of',
'childof',
'children-of',
'childrenof',
'parent-of',
'parentof',
'parents-of',
'parentsof',
'partof',
]
for keyword in keywords:
add_dependencies(keyword)
...@@ -18,9 +18,10 @@ ...@@ -18,9 +18,10 @@
import re import re
from topydo.lib.Colors import NEUTRAL_COLOR, Colors from topydo.lib.Color import AbstractColor
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib.PrettyPrinterFilter import PrettyPrinterFilter from topydo.lib.PrettyPrinterFilter import PrettyPrinterFilter
from topydo.lib.TopydoString import TopydoString
class PrettyPrinterColorFilter(PrettyPrinterFilter): class PrettyPrinterColorFilter(PrettyPrinterFilter):
...@@ -33,35 +34,26 @@ class PrettyPrinterColorFilter(PrettyPrinterFilter): ...@@ -33,35 +34,26 @@ class PrettyPrinterColorFilter(PrettyPrinterFilter):
def filter(self, p_todo_str, p_todo): def filter(self, p_todo_str, p_todo):
""" Applies the colors. """ """ Applies the colors. """
if config().colors(): if config().colors():
colorscheme = Colors() p_todo_str = TopydoString(p_todo_str, p_todo)
priority_color = colorscheme.get_priority_color(p_todo.priority())
project_color = colorscheme.get_project_color()
context_color = colorscheme.get_context_color()
metadata_color = colorscheme.get_metadata_color()
link_color = colorscheme.get_link_color()
# color projects / contexts priority_color = config().priority_color(p_todo.priority())
p_todo_str = re.sub(
r'\B(\+|@)(\S*\w)',
lambda m: (
context_color if m.group(0)[0] == "@"
else project_color) + m.group(0) + priority_color,
p_todo_str)
# tags colors = [
p_todo_str = re.sub(r'\b\S+:[^/\s]\S*\b', (r'\B@(\S*\w)', AbstractColor.CONTEXT),
metadata_color + r'\g<0>' + priority_color, (r'\B\+(\S*\w)', AbstractColor.PROJECT),
p_todo_str) (r'\b\S+:[^/\s]\S*\b', AbstractColor.META),
(r'(^|\s)(\w+:){1}(//\S+)', AbstractColor.LINK),
]
# add link_color to any valid URL specified outside of the tag. # color by priority
p_todo_str = re.sub(r'(^|\s)(\w+:){1}(//\S+)', p_todo_str.set_color(0, priority_color)
r'\1' + link_color + r'\2\3' + priority_color,
p_todo_str)
p_todo_str += NEUTRAL_COLOR for pattern, color in colors:
for match in re.finditer(pattern, p_todo_str.data):
p_todo_str.set_color(match.start(), color)
p_todo_str.set_color(match.end(), priority_color)
# color by priority p_todo_str.append('', AbstractColor.NEUTRAL)
p_todo_str = priority_color + p_todo_str
return p_todo_str return p_todo_str
...@@ -25,7 +25,7 @@ from topydo.lib.ListFormat import ListFormatParser ...@@ -25,7 +25,7 @@ from topydo.lib.ListFormat import ListFormatParser
class PrettyPrinterFormatFilter(PrettyPrinterFilter): class PrettyPrinterFormatFilter(PrettyPrinterFilter):
def __init__(self, p_todolist, p_format=None): def __init__(self, p_todolist, p_format=None):
super(PrettyPrinterFormatFilter, self).__init__() super().__init__()
self.parser = ListFormatParser(p_todolist, p_format) self.parser = ListFormatParser(p_todolist, p_format)
def filter(self, p_todo_str, p_todo): def filter(self, p_todo_str, p_todo):
......
...@@ -23,7 +23,7 @@ class PrettyPrinterNumbers(PrettyPrinterFilter): ...@@ -23,7 +23,7 @@ class PrettyPrinterNumbers(PrettyPrinterFilter):
""" Prepends the todo's number, retrieved from the todolist. """ """ Prepends the todo's number, retrieved from the todolist. """
def __init__(self, p_todolist): def __init__(self, p_todolist):
super(PrettyPrinterNumbers, self).__init__() super().__init__()
self.todolist = p_todolist self.todolist = p_todolist
def filter(self, p_todo_str, p_todo): def filter(self, p_todo_str, p_todo):
......
...@@ -21,8 +21,8 @@ notation. Useful for displaying dependencies. ...@@ -21,8 +21,8 @@ notation. Useful for displaying dependencies.
from textwrap import wrap from textwrap import wrap
from topydo.lib.Colorblock import progress_html_color
from topydo.lib.printers.PrettyPrinter import Printer from topydo.lib.printers.PrettyPrinter import Printer
from topydo.lib.ProgressColor import progress_color
from topydo.lib.Utils import humanize_date from topydo.lib.Utils import humanize_date
...@@ -89,13 +89,13 @@ class DotPrinter(Printer): ...@@ -89,13 +89,13 @@ class DotPrinter(Printer):
# print todos # print todos
for todo in p_todos: for todo in p_todos:
foreground, background = progress_html_color(todo) color = progress_color(todo).as_html()
result += ' {} [label={} style=filled fillcolor="{}" fontcolor="{}"]\n'.format( result += ' {} [label={} style=filled fillcolor="{}" fontcolor="{}"]\n'.format(
node_name(todo), node_name(todo),
node_label(todo), node_label(todo),
background, color,
foreground, '#000000',
) )
# print edges # print edges
......
...@@ -66,7 +66,7 @@ class IcalPrinter(Printer): ...@@ -66,7 +66,7 @@ class IcalPrinter(Printer):
""" """
def __init__(self, p_todolist): def __init__(self, p_todolist):
super(IcalPrinter, self).__init__() super().__init__()
self.todolist = p_todolist self.todolist = p_todolist
try: try:
...@@ -112,7 +112,7 @@ class IcalPrinter(Printer): ...@@ -112,7 +112,7 @@ class IcalPrinter(Printer):
if not uid: if not uid:
uid = generate_uid() uid = generate_uid()
p_todo.set_tag('ical', uid) p_todo.set_tag('ical', uid)
self.todolist.set_dirty() self.todolist.dirty = True
return uid return uid
......
...@@ -52,7 +52,7 @@ class JsonPrinter(Printer): ...@@ -52,7 +52,7 @@ class JsonPrinter(Printer):
""" """
def __init__(self): def __init__(self):
super(JsonPrinter, self).__init__() super().__init__()
def print_todo(self, p_todo): def print_todo(self, p_todo):
return json.dumps(_convert_todo(p_todo), ensure_ascii=False, return json.dumps(_convert_todo(p_todo), ensure_ascii=False,
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
from topydo.lib.prettyprinters.Colors import PrettyPrinterColorFilter from topydo.lib.prettyprinters.Colors import PrettyPrinterColorFilter
from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers
from topydo.lib.TopydoString import TopydoString
class Printer(object): class Printer(object):
...@@ -29,11 +30,8 @@ class Printer(object): ...@@ -29,11 +30,8 @@ class Printer(object):
raise NotImplementedError raise NotImplementedError
def print_list(self, p_todos): def print_list(self, p_todos):
""" for todo in p_todos:
Given a list of todo items, pretty print it and return a list of self.print_todo(todo)
formatted strings.
"""
return "\n".join([self.print_todo(todo) for todo in p_todos])
class PrettyPrinter(Printer): class PrettyPrinter(Printer):
...@@ -50,7 +48,7 @@ class PrettyPrinter(Printer): ...@@ -50,7 +48,7 @@ class PrettyPrinter(Printer):
""" """
Constructor. Constructor.
""" """
super(PrettyPrinter, self).__init__() super().__init__()
self.filters = [] self.filters = []
def add_filter(self, p_filter): def add_filter(self, p_filter):
...@@ -68,7 +66,15 @@ class PrettyPrinter(Printer): ...@@ -68,7 +66,15 @@ class PrettyPrinter(Printer):
for ppf in self.filters: for ppf in self.filters:
todo_str = ppf.filter(todo_str, p_todo) todo_str = ppf.filter(todo_str, p_todo)
return todo_str return TopydoString(todo_str)
def print_list(self, p_todos):
"""
Given a list of todo items, pretty print it and return a list of
formatted TopydoStrings. The output function in the UI should convert
the colors inside properly.
"""
return [self.print_todo(todo) for todo in p_todos]
def pretty_printer_factory(p_todolist, p_additional_filters=None): def pretty_printer_factory(p_todolist, p_additional_filters=None):
......
...@@ -22,7 +22,10 @@ I/O on the command-line. ...@@ -22,7 +22,10 @@ I/O on the command-line.
import getopt import getopt
import sys import sys
MAIN_OPTS = "ac:d:ht:v" from topydo.lib.Color import AbstractColor, Color
from topydo.lib.TopydoString import TopydoString
MAIN_OPTS = "ac:C:d:ht:v"
READ_ONLY_COMMANDS = ('List', 'ListContext', 'ListProject') READ_ONLY_COMMANDS = ('List', 'ListContext', 'ListProject')
...@@ -30,12 +33,14 @@ def usage(): ...@@ -30,12 +33,14 @@ def usage():
""" Prints the command-line usage of topydo. """ """ Prints the command-line usage of topydo. """
print("""\ print("""\
Synopsis: topydo [-a] [-c <config>] [-d <archive>] [-t <todo.txt>] subcommand [help|args] Synopsis: topydo [-a] [-c <config>] [-C <colormode>] [-d <archive>] [-t <todo.txt>] subcommand [help|args]
topydo -h topydo -h
topydo -v topydo -v
-a : Do not archive todo items on completion. -a : Do not archive todo items on completion.
-c : Specify an alternative configuration file. -c : Specify an alternative configuration file.
-C : Specify color mode (0 = disable, 1 = enable 16 colors,
16 = enable 16 colors, 256 = enable 256 colors, auto (default))
-d : Specify an alternative archive file (done.txt) -d : Specify an alternative archive file (done.txt)
-h : This help text -h : This help text
-t : Specify an alternative todo file -t : Specify an alternative todo file
...@@ -59,23 +64,62 @@ Available commands: ...@@ -59,23 +64,62 @@ Available commands:
* sort * sort
* tag * tag
Run `topydo help <subcommand>` for command-specific help. Run `topydo help <subcommand>` for command-specific help.\
""") """)
def write(p_file, p_string): def write(p_file, p_string):
""" """
Write p_string to file p_file, trailed by a newline character. Write p_string to file p_file, trailed by a newline character.
ANSI codes are removed when the file is not a TTY. ANSI codes are removed when the file is not a TTY (and colors are
automatically determined).
""" """
if not p_file.isatty(): if not config().colors(p_file.isatty()):
p_string = escape_ansi(p_string) p_string = escape_ansi(p_string)
if p_string: if p_string:
p_file.write(p_string + "\n") p_file.write(p_string + "\n")
def lookup_color(p_color):
"""
Converts an AbstractColor to a normal Color. Returns the Color itself
when a normal color is passed.
"""
if not lookup_color.colors:
lookup_color.colors[AbstractColor.NEUTRAL] = Color('NEUTRAL')
lookup_color.colors[AbstractColor.PROJECT] = config().project_color()
lookup_color.colors[AbstractColor.CONTEXT] = config().context_color()
lookup_color.colors[AbstractColor.META] = config().metadata_color()
lookup_color.colors[AbstractColor.LINK] = config().link_color()
try:
return lookup_color.colors[p_color]
except KeyError:
return p_color
lookup_color.colors = {}
def insert_ansi(p_string):
""" Returns a string with color information at the right positions. """
result = p_string.data
for pos, color in sorted(p_string.colors.items(), reverse=True):
color = lookup_color(color)
result = result[:pos] + color.as_ansi() + result[pos:]
return result
def output(p_string):
if isinstance(p_string, list):
p_string = "\n".join([insert_ansi(s) for s in p_string])
elif isinstance(p_string, TopydoString):
# convert color codes to ANSI
p_string = insert_ansi(p_string)
write(sys.stdout, p_string)
def error(p_string): def error(p_string):
""" Writes an error on the standard error. """ """ Writes an error on the standard error. """
write(sys.stderr, p_string) write(sys.stderr, p_string)
...@@ -140,6 +184,9 @@ class CLIApplicationBase(object): ...@@ -140,6 +184,9 @@ class CLIApplicationBase(object):
self.do_archive = False self.do_archive = False
elif opt == "-c": elif opt == "-c":
alt_config_path = value alt_config_path = value
elif opt == "-C":
overrides[('topydo', 'force_colors')] = '1'
overrides[('topydo', 'colors')] = value
elif opt == "-t": elif opt == "-t":
overrides[('topydo', 'filename')] = value overrides[('topydo', 'filename')] = value
elif opt == "-d": elif opt == "-d":
...@@ -174,7 +221,7 @@ class CLIApplicationBase(object): ...@@ -174,7 +221,7 @@ class CLIApplicationBase(object):
command = ArchiveCommand(self.todolist, archive) command = ArchiveCommand(self.todolist, archive)
command.execute() command.execute()
if archive.is_dirty(): if archive.dirty:
archive_file.write(archive.print_todos()) archive_file.write(archive.print_todos())
def _help(self, args): def _help(self, args):
...@@ -189,21 +236,24 @@ class CLIApplicationBase(object): ...@@ -189,21 +236,24 @@ class CLIApplicationBase(object):
READ_ONLY_COMMANDS) READ_ONLY_COMMANDS)
return p_command.__module__.endswith(read_only_commands) return p_command.__module__.endswith(read_only_commands)
def _execute(self, p_command, p_args): def _backup(self, p_command, p_args):
"""
Execute a subcommand with arguments. p_command is a class (not an
object).
"""
if config().backup_count() > 0 and p_command and not self.is_read_only(p_command): if config().backup_count() > 0 and p_command and not self.is_read_only(p_command):
call = [p_command.__module__.lower()[16:-7]] + p_args # strip "topydo.commands" and "Command" call = [p_command.__module__.lower()[16:-7]] + p_args # strip "topydo.commands" and "Command"
from topydo.lib.ChangeSet import ChangeSet from topydo.lib.ChangeSet import ChangeSet
self.backup = ChangeSet(self.todolist, p_call=call) self.backup = ChangeSet(self.todolist, p_call=call)
def _execute(self, p_command, p_args):
"""
Execute a subcommand with arguments. p_command is a class (not an
object).
"""
self._backup(p_command, p_args)
command = p_command( command = p_command(
p_args, p_args,
self.todolist, self.todolist,
lambda o: write(sys.stdout, o), output,
error, error,
input) input)
...@@ -219,9 +269,9 @@ class CLIApplicationBase(object): ...@@ -219,9 +269,9 @@ class CLIApplicationBase(object):
to the todo.txt file. to the todo.txt file.
""" """
# do not archive when the value of the filename is an empty string if self.todolist.dirty:
# (i.e. explicitly left empty in the configuration # do not archive when the value of the filename is an empty string
if self.todolist.is_dirty(): # (i.e. explicitly left empty in the configuration
if self.do_archive and config().archive(): if self.do_archive and config().archive():
self._archive() self._archive()
...@@ -233,6 +283,7 @@ class CLIApplicationBase(object): ...@@ -233,6 +283,7 @@ class CLIApplicationBase(object):
self.backup.save(self.todolist) self.backup.save(self.todolist)
self.todofile.write(self.todolist.print_todos()) self.todofile.write(self.todolist.print_todos())
self.todolist.dirty = False
self.backup = None self.backup = None
......
...@@ -19,8 +19,8 @@ ...@@ -19,8 +19,8 @@
import getopt import getopt
import sys import sys
from topydo.cli.CLI import CLIApplication from topydo.ui.cli.CLI import CLIApplication
from topydo.cli.CLIApplicationBase import MAIN_OPTS, error from topydo.ui.CLIApplicationBase import MAIN_OPTS, error
# enable color on windows CMD # enable color on windows CMD
if "win32" in sys.platform: if "win32" in sys.platform:
...@@ -41,10 +41,16 @@ def main(): ...@@ -41,10 +41,16 @@ def main():
if args[0] == 'prompt': if args[0] == 'prompt':
try: try:
from topydo.cli.Prompt import PromptApplication from topydo.ui.prompt.Prompt import PromptApplication
PromptApplication().run() PromptApplication().run()
except ImportError: except ImportError:
error("You have to install prompt-toolkit to run prompt mode.") error("You have to install prompt-toolkit to run prompt mode.")
elif args[0] == 'columns':
try:
from topydo.ui.columns.Main import UIApplication
UIApplication().run()
except ImportError:
error("You have to install urwid to run column mode.")
else: else:
CLIApplication().run() CLIApplication().run()
except IndexError: except IndexError:
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
import sys import sys
from topydo.cli.CLIApplicationBase import CLIApplicationBase, error from topydo.ui.CLIApplicationBase import CLIApplicationBase, error
from topydo.lib import TodoFile from topydo.lib import TodoFile
from topydo.lib.Config import config, ConfigError from topydo.lib.Config import config, ConfigError
...@@ -41,7 +41,7 @@ class CLIApplication(CLIApplicationBase): ...@@ -41,7 +41,7 @@ class CLIApplication(CLIApplicationBase):
""" """
def __init__(self): def __init__(self):
super(CLIApplication, self).__init__() super().__init__()
def run(self): def run(self):
""" Main entry function. """ """ Main entry function. """
...@@ -50,7 +50,11 @@ class CLIApplication(CLIApplicationBase): ...@@ -50,7 +50,11 @@ class CLIApplication(CLIApplicationBase):
self.todofile = TodoFile.TodoFile(config().todotxt()) self.todofile = TodoFile.TodoFile(config().todotxt())
self.todolist = TodoList.TodoList(self.todofile.read()) 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: if subcommand is None:
self._usage() self._usage()
......
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import configparser
from topydo.lib.Config import home_config_path, config
def columns():
"""
Returns list with complete column configuration dicts.
"""
def _get_column_dict(p_cp, p_column):
column_dict = dict()
column_dict['title'] = p_cp.get(p_column, 'title')
column_dict['filterexpr'] = p_cp.get(p_column, 'filterexpr')
column_dict['sortexpr'] = p_cp.get(p_column, 'sortexpr')
column_dict['show_all'] = p_cp.getboolean(p_column, 'show_all')
return column_dict
defaults = {
'title': 'Yet another column',
'filterexpr': '',
'sortexpr': config().sort_string(),
'show_all': '0',
}
cp = configparser.RawConfigParser(defaults)
files = [
"topydo_columns.ini",
"topydo_columns.conf",
".topydo_columns",
home_config_path('.topydo_columns'),
home_config_path('.config/topydo/columns'),
"/etc/topydo_columns.conf",
]
for filename in files:
if cp.read(filename):
break
column_list = []
for column in cp.sections():
column_list.append(_get_column_dict(cp, column))
return column_list
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import urwid
class CommandLineWidget(urwid.Edit):
def __init__(self, *args, **kwargs):
self.history = []
self.history_pos = None
# temporary history storage for edits before cmd execution
self.history_tmp = []
super().__init__(*args, **kwargs)
urwid.register_signal(CommandLineWidget, ['blur', 'execute_command'])
def clear(self):
self.set_edit_text("")
def _blur(self):
self.clear()
urwid.emit_signal(self, 'blur')
def _emit_command(self):
if len(self.edit_text) > 0:
urwid.emit_signal(self, 'execute_command', self.edit_text)
self._save_to_history()
self.clear()
def _save_to_history(self):
if len(self.edit_text) > 0:
self.history.append(self.edit_text)
self.history_pos = len(self.history)
self.history_tmp = self.history[:] # sync temporary storage with real history
self.history_tmp.append('')
def _history_move(self, p_step):
"""
Changes current value of the command-line to the value obtained from
history_tmp list with index calculated by addition of p_step to the
current position in the command history (history_pos attribute).
Also saves value of the command-line (before changing it) to history_tmp
for potential later access.
"""
if len(self.history) > 0:
# don't pollute real history - use temporary storage
self.history_tmp[self.history_pos] = self.edit_text
self.history_pos = self.history_pos + p_step
self.set_edit_text(self.history_tmp[self.history_pos])
def _history_next(self):
if self.history_pos != len(self.history):
self._history_move(1)
def _history_back(self):
if self.history_pos != 0:
self._history_move(-1)
def keypress(self, p_size, p_key):
dispatch = {
'enter': self._emit_command,
'esc': self._blur,
'up': self._history_back,
'down': self._history_next
}
try:
dispatch[p_key]()
except KeyError:
super().keypress(p_size, p_key)
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import urwid
from topydo.lib.Color import AbstractColor
from topydo.lib.Todo import Todo
from topydo.lib.TopydoString import TopydoString
from topydo.ui.columns.Utils import PaletteItem
PALETTE_LOOKUP = {
# omitting AbstractColor.NEUTRAL on purpose, so a text without any
# attribute will be added to the markup
AbstractColor.PROJECT: PaletteItem.PROJECT,
AbstractColor.CONTEXT: PaletteItem.CONTEXT,
AbstractColor.META: PaletteItem.METADATA,
AbstractColor.LINK: PaletteItem.LINK,
}
def topydostringToMarkup(p_string):
markup = []
color_positions = sorted(p_string.colors.items())
# in case no color positions are available, at least set something at the
# start position
if not color_positions:
color_positions = [(0, None)]
for i, (start_pos, color) in enumerate(color_positions):
# color starts at indicated position
start = start_pos
# color ends at next color indication. if missing, run until the end of
# the string
try:
end = color_positions[i+1][0]
except IndexError:
end = len(str(p_string))
text = str(p_string)[start:end]
if color in PALETTE_LOOKUP:
markup.append((PALETTE_LOOKUP[color], text))
else:
# a plain text without any attribute set (including
# AbstractColor.NEUTRAL)
markup.append(text)
color_at_start = color_positions and color_positions[0][0] == 0
# priority color should appear at the start if present, build a nesting
# markup
if color_at_start and isinstance(p_string.metadata, Todo):
priority = p_string.metadata.priority()
markup = ('pri_' + priority, markup)
return markup
class ConsoleWidget(urwid.LineBox):
def __init__(self, p_text=""):
urwid.register_signal(ConsoleWidget, ['close'])
self.width = 0
self.pile = urwid.Pile([])
super().__init__(self.pile)
def keypress(self, p_size, p_key):
if p_key == 'enter' or p_key == 'q' or p_key == 'esc':
urwid.emit_signal(self, 'close')
# don't return the key, 'enter', 'escape', 'q' or ':' are your only
# escape. ':' will reenter to the cmdline.
elif p_key == ':':
urwid.emit_signal(self, 'close', True)
def render(self, p_size, focus):
"""
This override intercepts the width of the widget such that it can be
stored. The width is used for rendering `ls` output.
"""
self.width = p_size[0]
return super().render(p_size, focus)
def selectable(self):
return True
def print_text(self, p_text):
if isinstance(p_text, list):
for text in p_text:
self.print_text(text)
return
elif isinstance(p_text, TopydoString):
text = urwid.Text(topydostringToMarkup(p_text))
else:
text = urwid.Text(p_text)
self.pile.contents.append((text, ('pack', None)))
def clear(self):
self.pile.contents = []
def console_width(self):
# return the last known width (last render)
return self.width
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import urwid
class KeystateWidget(urwid.Text):
def __init__(self):
super().__init__('', align='right')
def selectable(self):
return False
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import datetime
import shlex
import time
import urwid
from collections import namedtuple
from string import ascii_uppercase
from topydo.Commands import get_subcommand
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
from topydo.lib.View import View
from topydo.lib import TodoFile
from topydo.lib import TodoList
from topydo.ui.CLIApplicationBase import CLIApplicationBase
from topydo.ui.columns.CommandLineWidget import CommandLineWidget
from topydo.ui.columns.ConsoleWidget import ConsoleWidget
from topydo.ui.columns.KeystateWidget import KeystateWidget
from topydo.ui.columns.TodoListWidget import TodoListWidget
from topydo.ui.columns.Utils import PaletteItem, to_urwid_color
from topydo.ui.columns.ViewWidget import ViewWidget
from topydo.ui.columns.ColumnLayout import columns
COLUMN_WIDTH = 40
class UIView(View):
"""
A subclass of view holding user input data that constructed the view (i.e.
the sort expression and the filter expression, etc.)
"""
def __init__(self, p_sorter, p_filter, p_todolist, p_data):
super().__init__(p_sorter, p_filter, p_todolist)
self.data = p_data
_APPEND_COLUMN = 1
_EDIT_COLUMN = 2
_COPY_COLUMN = 3
_INSERT_COLUMN = 4
class MainPile(urwid.Pile):
"""
This subclass of Pile doesn't change focus on cursor up/down / mouse press
events. The implementation was taken from its base class.
"""
def __init__(self, p_widget_list, p_focus_item=None):
urwid.register_signal(MainPile, ['blur_console'])
super().__init__(p_widget_list, p_focus_item)
def mouse_event(self, p_size, p_event, p_button, p_col, p_row, p_focus):
if self.focus_position != 2:
urwid.emit_signal(self, 'blur_console')
return super().mouse_event(p_size, p_event, p_button, p_col, p_row, p_focus) # pylint: disable=E1102
def keypress(self, p_size, p_key):
if not self.contents:
return p_key
item_rows = None
if len(p_size) == 2:
item_rows = self.get_item_rows(p_size, focus=True)
i = self.focus_position
if self.selectable():
tsize = self.get_item_size(p_size, i, True, item_rows)
key = self.focus.keypress(tsize, p_key)
if self._command_map[key] not in ('cursor up', 'cursor down'):
return key
class UIApplication(CLIApplicationBase):
def __init__(self):
super().__init__()
self._process_flags()
self.column_width = config().column_width()
self.todofile = TodoFile.TodoFile(config().todotxt())
self.todolist = TodoList.TodoList(self.todofile.read())
self.marked_todos = []
self.columns = urwid.Columns([], dividechars=0,
min_width=config().column_width())
self.commandline = CommandLineWidget('topydo> ')
self.keystate_widget = KeystateWidget()
self.status_line = urwid.Columns([
('weight', 1, urwid.Filler(self.commandline)),
])
self.keymap = config().column_keymap()
self._alarm = None
self._last_cmd = None
# console widget
self.console = ConsoleWidget()
get_terminal_size(self._console_width)
urwid.connect_signal(self.commandline, 'blur', self._blur_commandline)
urwid.connect_signal(self.commandline, 'execute_command',
self._execute_handler)
def hide_console(p_focus_commandline=False):
self._console_visible = False
if p_focus_commandline:
self._focus_commandline()
urwid.connect_signal(self.console, 'close', hide_console)
# view widget
self.viewwidget = ViewWidget(self.todolist)
urwid.connect_signal(self.viewwidget, 'save',
lambda: self._update_view(self.viewwidget.data))
def hide_viewwidget():
self._viewwidget_visible = False
self._blur_commandline()
urwid.connect_signal(self.viewwidget, 'close', hide_viewwidget)
self.mainwindow = MainPile([
('weight', 1, self.columns),
(1, self.status_line),
])
urwid.connect_signal(self.mainwindow, 'blur_console', hide_console)
# the columns should have keyboard focus
self._blur_commandline()
self._screen = urwid.raw_display.Screen()
if config().colors():
self._screen.register_palette(self._create_color_palette())
else:
self._screen.register_palette(self._create_mono_palette())
self._screen.set_terminal_properties(256)
self.mainloop = urwid.MainLoop(
self.mainwindow,
screen=self._screen,
unhandled_input=self._handle_input,
pop_ups=True
)
self.column_mode = _APPEND_COLUMN
self._set_alarm_for_next_midnight_update()
def _create_color_palette(self):
project_color = to_urwid_color(config().project_color())
context_color = to_urwid_color(config().context_color())
metadata_color = to_urwid_color(config().metadata_color())
link_color = to_urwid_color(config().link_color())
palette = [
(PaletteItem.PROJECT, '', '', '', project_color, ''),
(PaletteItem.PROJECT_FOCUS, '', 'light gray', '', project_color, None),
(PaletteItem.CONTEXT, '', '', '', context_color, ''),
(PaletteItem.CONTEXT_FOCUS, '', 'light gray', '', context_color, None),
(PaletteItem.METADATA, '', '', '', metadata_color, ''),
(PaletteItem.METADATA_FOCUS, '', 'light gray', '', metadata_color, None),
(PaletteItem.LINK, '', '', '', link_color, ''),
(PaletteItem.LINK_FOCUS, '', 'light gray', '', link_color, None),
(PaletteItem.DEFAULT_FOCUS, 'black', 'light gray'),
(PaletteItem.MARKED, '', 'light blue'),
]
for C in ascii_uppercase:
pri_color_cfg = config().priority_color(C)
pri_color = to_urwid_color(pri_color_cfg)
pri_color_focus = pri_color if not pri_color_cfg.is_neutral() else 'black'
palette.append((
'pri_' + C, '', '', '', pri_color, ''
))
palette.append((
'pri_' + C + '_focus', '', 'light gray', '', pri_color_focus, None
))
return palette
def _create_mono_palette(self):
palette = [
(PaletteItem.DEFAULT_FOCUS, 'black', 'light gray'),
(PaletteItem.PROJECT_FOCUS, PaletteItem.DEFAULT_FOCUS),
(PaletteItem.CONTEXT_FOCUS, PaletteItem.DEFAULT_FOCUS),
(PaletteItem.METADATA_FOCUS, PaletteItem.DEFAULT_FOCUS),
(PaletteItem.LINK_FOCUS, PaletteItem.DEFAULT_FOCUS),
(PaletteItem.MARKED, 'default,underline,bold', 'default'),
]
for C in ascii_uppercase:
palette.append(
('pri_' + C + '_focus', PaletteItem.DEFAULT_FOCUS)
)
return palette
def _set_alarm_for_next_midnight_update(self):
def callback(p_loop, p_data):
self._update_all_columns()
self._set_alarm_for_next_midnight_update()
tomorrow = datetime.datetime.now() + datetime.timedelta(days=1)
# turn it into midnight
tomorrow = tomorrow.replace(hour=0, minute=0, second=0, microsecond=0)
self.mainloop.set_alarm_at(time.mktime(tomorrow.timetuple()), callback)
def _output(self, p_text):
self._print_to_console(p_text)
def _execute_handler(self, p_command, p_todo_id=None, p_output=None):
"""
Executes a command, given as a string.
"""
p_output = p_output or self._output
self._last_cmd = (p_command, p_output == self._output)
if '{}' in p_command:
if self._has_marked_todos():
p_todo_id = ' '.join(self.marked_todos)
p_command = p_command.format(p_todo_id)
try:
p_command = shlex.split(p_command)
except ValueError as verr:
self._print_to_console('Error: ' + str(verr))
return
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)
try:
command = subcommand(
args,
self.todolist,
p_output,
self._output,
self._input,
)
if command.execute() != False:
self._post_execute()
except TypeError:
# TODO: show error message
pass
def _update_all_columns(self):
for column, _ in self.columns.contents:
column.update()
column.keystate = None
def _post_execute(self):
# store dirty flag because base _post_execute will reset it after flush
dirty = self.todolist.dirty
super()._post_execute()
if dirty or self.marked_todos:
self._reset_state()
def _repeat_last_cmd(self, p_todo_id=None):
try:
cmd, verbosity = self._last_cmd
except TypeError:
return
self._execute_handler(cmd, p_todo_id,
self._output if verbosity else lambda _: None)
def _reset_state(self):
self.marked_todos = []
self._update_all_columns()
def _blur_commandline(self):
self.mainwindow.focus_item = 0
def _focus_commandline(self):
self.mainwindow.focus_item = 1
def _focus_first_column(self):
self.columns.focus_position = 0
def _focus_last_column(self):
end_pos = len(self.columns.contents) - 1
self.columns.focus_position = end_pos
def _focus_next_column(self):
size = len(self.columns.contents)
if self.columns.focus_position < size -1:
self.columns.focus_position += 1
def _focus_previous_column(self):
if self.columns.focus_position > 0:
self.columns.focus_position -= 1
def _append_column(self):
self.viewwidget.reset()
self.column_mode = _APPEND_COLUMN
self._viewwidget_visible = True
def _insert_column(self):
self.viewwidget.reset()
self.column_mode = _INSERT_COLUMN
self._viewwidget_visible = True
def _edit_column(self):
self.viewwidget.data = self.columns.focus.view.data
self.column_mode = _EDIT_COLUMN
self._viewwidget_visible = True
def _delete_column(self):
try:
focus = self.columns.focus_position
del self.columns.contents[focus]
if self.columns.contents:
self.columns.focus_position = focus
else:
self._focus_commandline()
except IndexError:
# no columns
pass
def _copy_column(self):
self.viewwidget.data = self.columns.focus.view.data
self.column_mode = _COPY_COLUMN
self._viewwidget_visible = True
def _column_action_handler(self, p_action):
dispatch = {
'first_column': self._focus_first_column,
'last_column': self._focus_last_column,
'prev_column': self._focus_previous_column,
'next_column': self._focus_next_column,
'append_column': self._append_column,
'insert_column': self._insert_column,
'edit_column': self._edit_column,
'delete_column': self._delete_column,
'copy_column': self._copy_column,
'swap_left': self._swap_column_left,
'swap_right': self._swap_column_right,
'reset': self._reset_state,
}
dispatch[p_action]()
def _handle_input(self, p_input):
dispatch = {
':': self._focus_commandline,
}
try:
dispatch[p_input]()
except KeyError:
# the key is unknown, ignore
pass
def _viewdata_to_view(self, p_data):
"""
Converts a dictionary describing a view to an actual UIView instance.
"""
sorter = Sorter(p_data['sortexpr'])
filters = []
if not p_data['show_all']:
filters.append(DependencyFilter(self.todolist))
filters.append(RelevanceFilter())
filters += get_filter_list(p_data['filterexpr'].split())
return UIView(sorter, filters, self.todolist, p_data)
def _update_view(self, p_data):
""" Creates a view from the data entered in the view widget. """
view = self._viewdata_to_view(p_data)
if self.column_mode == _APPEND_COLUMN or self.column_mode == _COPY_COLUMN:
self._add_column(view)
elif self.column_mode == _INSERT_COLUMN:
self._add_column(view, self.columns.focus_position)
elif self.column_mode == _EDIT_COLUMN:
current_column = self.columns.focus
current_column.title = p_data['title']
current_column.view = view
self._viewwidget_visible = False
self._blur_commandline()
def _add_column(self, p_view, p_pos=None):
"""
Given an UIView, adds a new column widget with the todos in that view.
When no position is given, it is added to the end, otherwise inserted
before that position.
"""
def execute_silent(p_cmd, p_todo_id=None):
self._execute_handler(p_cmd, p_todo_id, lambda _: None)
todolist = TodoListWidget(p_view, p_view.data['title'], self.keymap)
urwid.connect_signal(todolist, 'execute_command_silent',
execute_silent)
urwid.connect_signal(todolist, 'execute_command', self._execute_handler)
urwid.connect_signal(todolist, 'repeat_cmd', self._repeat_last_cmd)
urwid.connect_signal(todolist, 'refresh', self.mainloop.screen.clear)
urwid.connect_signal(todolist, 'add_pending_action', self._set_alarm)
urwid.connect_signal(todolist, 'remove_pending_action', self._remove_alarm)
urwid.connect_signal(todolist, 'column_action', self._column_action_handler)
urwid.connect_signal(todolist, 'show_keystate', self._print_keystate)
urwid.connect_signal(todolist, 'toggle_mark',
self._process_mark_toggle)
options = self.columns.options(
width_type='given',
width_amount=config().column_width(),
box_widget=True
)
item = (todolist, options)
if p_pos == None:
p_pos = len(self.columns.contents)
self.columns.contents.insert(p_pos, item)
self.columns.focus_position = p_pos
self._blur_commandline()
def _print_keystate(self, p_keystate):
self.keystate_widget.set_text(p_keystate)
self._keystate_visible = len(p_keystate) > 0
def _set_alarm(self, p_callback):
""" Sets alarm to execute p_action specified in 0.5 sec. """
self._alarm = self.mainloop.set_alarm_in(0.5, p_callback)
def _remove_alarm(self):
""" Removes pending action alarm stored in _alarm attribute. """
self.mainloop.remove_alarm(self._alarm)
self._alarm = None
def _swap_column_left(self):
pos = self.columns.focus_position
if pos > 0:
_columns = self.columns.contents
_columns[pos], _columns[pos - 1] = _columns[pos - 1], _columns[pos]
self.columns.focus_position -= 1
def _swap_column_right(self):
pos = self.columns.focus_position
_columns = self.columns.contents
if pos < len(_columns) - 1:
_columns[pos], _columns[pos + 1] = _columns[pos + 1], _columns[pos]
self.columns.focus_position += 1
@property
def _console_visible(self):
contents = self.mainwindow.contents
return len(contents) == 3 and isinstance(contents[2][0], ConsoleWidget)
@_console_visible.setter
def _console_visible(self, p_enabled):
contents = self.mainwindow.contents
if p_enabled == True and len(contents) == 2:
contents.append((self.console, ('pack', None)))
self.mainwindow.focus_position = 2
elif p_enabled == False and self._console_visible:
self.console.clear()
del contents[2]
self.mainwindow.focus_position = 0
@property
def _keystate_visible(self):
contents = self.status_line.contents
return len(contents) == 2 and isinstance(contents[1][0].original_widget,
KeystateWidget)
@_keystate_visible.setter
def _keystate_visible(self, p_enabled):
contents = self.status_line.contents
if p_enabled and len(contents) == 1:
contents.append((urwid.Filler(self.keystate_widget),
('weight', 1, True)))
elif not p_enabled and self._keystate_visible:
del contents[1]
@property
def _viewwidget_visible(self):
contents = self.mainwindow.contents
return len(contents) == 3 and isinstance(contents[2][0], ViewWidget)
@_viewwidget_visible.setter
def _viewwidget_visible(self, p_enabled):
contents = self.mainwindow.contents
if p_enabled == True and len(contents) == 2:
contents.append((self.viewwidget, ('pack', None)))
self.mainwindow.focus_position = 2
elif p_enabled == False and self._viewwidget_visible:
del contents[2]
def _print_to_console(self, p_text):
self._console_visible = True
self.console.print_text(p_text)
def _input(self, p_question):
self._print_to_console(p_question)
# don't wait for the event loop to enter idle, there is a command
# waiting for input right now, so already go ahead and draw the
# question on screen.
self.mainloop.draw_screen()
user_input = self.mainloop.screen.get_input()
self._console_visible = False
return user_input[0]
def _console_width(self):
terminal_size = namedtuple('Terminal_Size', 'columns lines')
width = self.console.console_width() - 2
sz = terminal_size(width, 1)
return sz
def _has_marked_todos(self):
return len(self.marked_todos) > 0
def _process_mark_toggle(self, p_todo_id):
"""
Adds p_todo_id to marked_todos attribute and returns True if p_todo_id
is not already present. Removes p_todo_id from marked_todos and returns
False otherwise.
"""
if p_todo_id not in self.marked_todos:
self.marked_todos.append(p_todo_id)
return True
else:
self.marked_todos.remove(p_todo_id)
return False
def run(self):
layout = columns()
if len(layout) > 0:
for column in layout:
self._add_column(self._viewdata_to_view(column))
else:
dummy = {
"title": "All tasks",
"sortexpr": "desc:prio",
"filterexpr": "",
"show_all": True,
}
self._add_column(self._viewdata_to_view(dummy))
# make sure that the first column is focused on startup
self.columns.focus_position = 0
self.mainloop.run()
if __name__ == '__main__':
UIApplication().run()
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import urwid
from topydo.lib.Utils import translate_key_to_config
from topydo.ui.columns.TodoWidget import TodoWidget
def get_execute_signal(p_prefix):
if p_prefix == 'cmdv':
signal = 'execute_command'
else:
signal = 'execute_command_silent'
return signal
class TodoListWidget(urwid.LineBox):
def __init__(self, p_view, p_title, p_keymap):
self._view = None
self.keymap = p_keymap
# store a state for multi-key shortcuts (e.g. 'gg')
self.keystate = None
# store offset length for postpone command (e.g. '3' for 'p3w')
self._pp_offset = None
self._title_widget = urwid.Text(p_title, align='center')
self.todolist = urwid.SimpleFocusListWalker([])
self.listbox = urwid.ListBox(self.todolist)
self.view = p_view
pile = urwid.Pile([
(1, urwid.Filler(self._title_widget)),
(1, urwid.Filler(urwid.Divider('\u2500'))),
('weight', 1, self.listbox),
])
pile.focus_position = 2
super().__init__(pile)
urwid.register_signal(TodoListWidget, ['execute_command_silent',
'execute_command',
'refresh',
'add_pending_action',
'remove_pending_action',
'repeat_cmd',
'column_action',
'show_keystate',
'toggle_mark',
])
@property
def view(self):
return self._view
@view.setter
def view(self, p_view):
self._view = p_view
self.update()
@property
def title(self):
return self._title_widget.text
@title.setter
def title(self, p_title):
self._title_widget.set_text(p_title)
def update(self):
"""
Updates the todo list according to the todos in the view associated
with this list.
"""
old_focus_position = self.todolist.focus
del self.todolist[:]
for todo in self.view.todos:
todowidget = TodoWidget(todo, self.view.todolist.number(todo))
self.todolist.append(todowidget)
self.todolist.append(urwid.Divider('-'))
if old_focus_position:
try:
self.todolist.set_focus(old_focus_position)
except IndexError:
# scroll to the bottom if the last item disappeared from column
# -2 for the same reason as in self._scroll_to_bottom()
self.todolist.set_focus(len(self.todolist) - 2)
def _scroll_to_top(self, p_size):
self.listbox.set_focus(0)
# see comment at _scroll_to_bottom
self.listbox.calculate_visible(p_size)
def _scroll_to_bottom(self, p_size):
# -2 because the last Divider shouldn't be focused.
end_pos = len(self.listbox.body) - 2
self.listbox.set_focus(end_pos)
# for some reason, set_focus doesn't rerender the list.
# calculate_visible is the only public method (besides keypress) that
# deals with pending focus changes.
self.listbox.calculate_visible(p_size)
@property
def keystate(self):
return self._keystate
@keystate.setter
def keystate(self, p_keystate):
self._keystate = p_keystate
keystate_to_show = p_keystate if p_keystate else ''
urwid.emit_signal(self, 'show_keystate', keystate_to_show)
def keypress(self, p_size, p_key):
urwid.emit_signal(self, 'remove_pending_action')
requires_further_input = ['postpone', 'postpone_s', 'pri']
keymap, keystates = self.keymap
shortcut = self.keystate or ''
shortcut += translate_key_to_config(p_key)
try:
action = keymap[shortcut]
except KeyError:
action = None
if action:
if shortcut in keystates:
# Supplied key-shortcut matches keystate and action. Save the
# keystate in case user will hit another key and add an action
# waiting for execution if user won't type anything further.
self.keystate = shortcut
if action not in requires_further_input:
self._add_pending_action(action, p_size)
else:
# Only action is matched. Handle it and reset keystate.
self.resolve_action(action, p_size)
self.keystate = None
return
else:
if shortcut in keystates:
self.keystate = shortcut
else:
try:
# Check whether current keystate matches built-in 'postpone'
# action.
mode = keymap[self.keystate]
if mode in ['postpone', 'postpone_s']:
if self._postpone_selected(p_key, mode) is not None:
self.keystate = None
else:
urwid.emit_signal(self, 'show_keystate',
self.keystate + self._pp_offset)
else:
self.keystate = None
return
except KeyError:
if not self.keystate:
# Single key that is not described in keymap config.
return self.listbox.keypress(p_size, p_key)
self.keystate = None
return
def mouse_event(self, p_size, p_event, p_button, p_column, p_row, p_focus):
if p_event == 'mouse press':
if p_button == 4: # up
self.listbox.keypress(p_size, 'up')
return
elif p_button == 5: # down:
self.listbox.keypress(p_size, 'down')
return
return super().mouse_event(p_size, # pylint: disable=E1102
p_event,
p_button,
p_column,
p_row,
p_focus)
def selectable(self):
return True
def _toggle_marked_status(self):
try:
todo = self.listbox.focus.todo
todo_id = str(self.view.todolist.number(todo))
if urwid.emit_signal(self, 'toggle_mark', todo_id):
self.listbox.focus.mark()
else:
self.listbox.focus.unmark()
except AttributeError:
# No todo item selected
pass
def _execute_on_selected(self, p_cmd_str, p_execute_signal):
"""
Executes command specified by p_cmd_str on selected todo item.
p_cmd_str should be a string with one replacement field ('{}') which
will be substituted by id of the selected todo item.
p_execute_signal is the signal name passed to the main loop. It should
be one of 'execute_command' or 'execute_command_silent'.
"""
try:
todo = self.listbox.focus.todo
todo_id = str(self.view.todolist.number(todo))
urwid.emit_signal(self, p_execute_signal, p_cmd_str, todo_id)
# force screen redraw after editing
if p_cmd_str.startswith('edit'):
urwid.emit_signal(self, 'refresh')
except AttributeError:
# No todo item selected
pass
def resolve_action(self, p_action_str, p_size=None):
"""
Checks whether action specified in p_action_str is "built-in" or
contains topydo command (i.e. starts with 'cmd') and forwards it to
proper executing methods.
p_size should be specified for some of the builtin actions like 'up' or
'home' as they can interact with urwid.ListBox.keypress or
urwid.ListBox.calculate_visible.
"""
if p_action_str.startswith(('cmd ', 'cmdv ')):
prefix, cmd = p_action_str.split(' ', 1)
execute_signal = get_execute_signal(prefix)
if '{}' in cmd:
self._execute_on_selected(cmd, execute_signal)
else:
urwid.emit_signal(self, execute_signal, cmd)
else:
self.execute_builtin_action(p_action_str, p_size)
def execute_builtin_action(self, p_action_str, p_size=None):
"""
Executes built-in action specified in p_action_str.
Currently supported actions are: 'up', 'down', 'home', 'end',
'first_column', 'last_column', 'prev_column', 'next_column',
'append_column', 'insert_column', 'edit_column', 'delete_column',
'copy_column', swap_right', 'swap_left', 'postpone', 'postpone_s',
'pri', 'mark', 'reset' and 'repeat'.
"""
column_actions = ['first_column',
'last_column',
'prev_column',
'next_column',
'append_column',
'insert_column',
'edit_column',
'delete_column',
'copy_column',
'swap_left',
'swap_right',
'reset',
]
if p_action_str in column_actions:
urwid.emit_signal(self, 'column_action', p_action_str)
elif p_action_str in ['up', 'down']:
self.listbox.keypress(p_size, p_action_str)
elif p_action_str == 'home':
self._scroll_to_top(p_size)
elif p_action_str == 'end':
self._scroll_to_bottom(p_size)
elif p_action_str in ['postpone', 'postpone_s']:
pass
elif p_action_str == 'pri':
pass
elif p_action_str == 'mark':
self._toggle_marked_status()
elif p_action_str == 'repeat':
self._repeat_cmd()
def _add_pending_action(self, p_action, p_size):
"""
Creates action waiting for execution and forwards it to the mainloop.
"""
def generate_callback():
def callback(*args):
self.resolve_action(p_action, p_size)
self.keystate = None
return callback
urwid.emit_signal(self, 'add_pending_action', generate_callback())
def _postpone_selected(self, p_pattern, p_mode):
"""
Postpones selected todo item by <COUNT><PERIOD>.
Returns True after 'postpone' command is called (i.e. p_pattern is valid
<PERIOD>), False when p_pattern is invalid and None if p_pattern is
digit (i.e. part of <COUNT>).
p_pattern accepts digit (<COUNT>) or one of the <PERIOD> letters:
'd'(ay), 'w'(eek), 'm'(onth), 'y'(ear). If digit is specified, it is
appended to _pp_offset attribute. If p_pattern contains one of the
<PERIOD> letters, 'postpone' command is forwarded to execution with
value of _pp_offset attribute used as <COUNT>. If _pp_offset is None,
<COUNT> is set to 1.
p_mode should be one of 'postpone_s' or 'postpone'. It decides whether
'postpone' command should be called with or without '-s' flag.
"""
if p_pattern.isdigit():
if not self._pp_offset:
self._pp_offset = ''
self._pp_offset += p_pattern
result = None
else:
if p_pattern in ['d', 'w', 'm', 'y', 'b']:
offset = self._pp_offset or '1'
if p_mode == 'postpone':
pp_cmd = 'cmd postpone {} '
else:
pp_cmd = 'cmd postpone -s {} '
pp_cmd += offset + p_pattern
self.resolve_action(pp_cmd)
result = True
self._pp_offset = None
result = False
return result
def _repeat_cmd(self):
try:
todo = self.listbox.focus.todo
todo_id = str(self.view.todolist.number(todo))
except AttributeError:
todo_id = None
urwid.emit_signal(self, 'repeat_cmd', todo_id)
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
import urwid
from topydo.lib.Config import config
from topydo.lib.ListFormat import ListFormatParser
from topydo.lib.ProgressColor import progress_color
from topydo.ui.columns.Utils import PaletteItem, to_urwid_color
# pass a None todo list, since we won't use %i or %I here
PRIO_FORMATTER = ListFormatParser(None, "%{(}p{)}")
TEXT_FORMATTER = ListFormatParser(None, "%s %k\n%h")
PRJ_CON_PATTERN = r'\B(?:\+|@)(?:\S*\w)'
TAG_PATTERN = r'\b\S+:[^/\s]\S*\b'
URL_PATTERN = r'(?:^|\s)(?:\w+:){1}(?://\S+)'
def _markup(p_todo, p_focus):
"""
Returns an attribute spec for the colors that correspond to the given todo
item.
"""
pri = p_todo.priority()
pri = 'pri_' + pri if pri else PaletteItem.DEFAULT
if not p_focus:
attr_dict = {None: pri}
else:
# use '_focus' palette entries instead of standard ones
attr_dict = {None: pri + '_focus'}
attr_dict[PaletteItem.PROJECT] = PaletteItem.PROJECT_FOCUS
attr_dict[PaletteItem.CONTEXT] = PaletteItem.CONTEXT_FOCUS
attr_dict[PaletteItem.METADATA] = PaletteItem.METADATA_FOCUS
attr_dict[PaletteItem.LINK] = PaletteItem.LINK_FOCUS
return attr_dict
class TodoWidget(urwid.WidgetWrap):
def __init__(self, p_todo, p_number):
# clients use this to associate this widget with the given todo item
self.todo = p_todo
todo_text = TEXT_FORMATTER.parse(p_todo)
priority_text = PRIO_FORMATTER.parse(p_todo)
# split todo_text at each occurrence of tag/project/context/url
txt_pattern = r'|'.join([PRJ_CON_PATTERN, TAG_PATTERN, URL_PATTERN])
txt_pattern = r'(' + txt_pattern + r')'
txt_splitted = re.split(txt_pattern, todo_text)
txt_markup = []
# Examine each substring and apply relevant palette entry if needed
for substring in txt_splitted:
# re.split can generate empty strings when capturing group is used
if not substring:
continue
if re.match(TAG_PATTERN, substring):
txt_markup.append((PaletteItem.METADATA, substring))
elif re.match(URL_PATTERN, substring):
txt_markup.append((PaletteItem.LINK, substring))
elif re.match(PRJ_CON_PATTERN, substring):
if substring.startswith('+'):
txt_markup.append((PaletteItem.PROJECT, substring))
else:
txt_markup.append((PaletteItem.CONTEXT, substring))
else:
txt_markup.append(substring)
id_widget = urwid.Text(str(p_number), align='right')
priority_widget = urwid.Text(priority_text)
self.text_widget = urwid.Text(txt_markup)
progress = to_urwid_color(progress_color(p_todo)) if config().colors() else PaletteItem.DEFAULT
progress_bar = urwid.AttrMap(
urwid.SolidFill(' '),
urwid.AttrSpec(PaletteItem.DEFAULT, progress, 256),
urwid.AttrSpec(PaletteItem.DEFAULT, progress, 256),
)
self.columns = urwid.Columns(
[
(1, progress_bar),
(4, id_widget),
(3, priority_widget),
('weight', 1, self.text_widget),
],
dividechars=1,
box_columns=[0] # the progress bar adapts its height to the rest
)
self.widget = urwid.AttrMap(
self.columns,
_markup(p_todo, p_focus=False),
_markup(p_todo, p_focus=True)
)
super().__init__(self.widget)
def keypress(self, p_size, p_key):
"""
Override keypress to prevent the wrapped Columns widget to
receive any key.
"""
return p_key
def selectable(self):
# make sure that ListBox will highlight this widget
return True
def mark(self):
attr_map = {
None: PaletteItem.MARKED,
PaletteItem.LINK: PaletteItem.MARKED,
PaletteItem.CONTEXT: PaletteItem.MARKED,
PaletteItem.PROJECT: PaletteItem.MARKED,
PaletteItem.METADATA: PaletteItem.MARKED,
}
self.widget.set_attr_map(attr_map)
def unmark(self):
self.widget.set_attr_map(_markup(self.todo, False))
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2016 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class PaletteItem:
PROJECT = 'project'
PROJECT_FOCUS = 'project_focus'
CONTEXT = 'context'
CONTEXT_FOCUS = 'context_focus'
METADATA = 'metadata'
METADATA_FOCUS = 'metadata_focus'
LINK = 'link'
LINK_FOCUS = 'link_focus'
DEFAULT = 'default'
DEFAULT_FOCUS = 'default_focus'
MARKED = 'marked'
def to_urwid_color(p_color):
"""
Given a Color object, transform it to a color that urwid understands.
"""
if not p_color.is_valid():
return 'black'
elif p_color.is_neutral():
return 'default'
else:
return 'h{}'.format(p_color.color)
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import urwid
from topydo.lib.Config import config
class ViewWidget(urwid.LineBox):
def __init__(self, p_todolist):
self._todolist = p_todolist
self.titleedit = urwid.Edit("Title: ", "")
self.sortedit = urwid.Edit("Sort expression: ", "")
self.filteredit = urwid.Edit("Filter expression: ", "")
group = []
self.relevantradio = urwid.RadioButton(group, "Only show relevant todo items", True)
self.allradio = urwid.RadioButton(group, "Show all todo items")
self.pile = urwid.Pile([
self.filteredit,
self.titleedit,
self.sortedit,
self.relevantradio,
self.allradio,
urwid.Button("Save", lambda _: urwid.emit_signal(self, 'save')),
urwid.Button("Cancel", lambda _: self.close()),
])
self.reset()
super().__init__(self.pile)
urwid.register_signal(ViewWidget, ['save', 'close'])
@property
def data(self):
return {
'title': self.titleedit.edit_text or self.filteredit.edit_text,
'sortexpr': self.sortedit.edit_text or config().sort_string(),
'filterexpr': self.filteredit.edit_text,
'show_all': self.allradio.state,
}
@data.setter
def data(self, p_data):
self.titleedit.edit_text = p_data['title']
self.sortedit.edit_text = p_data['sortexpr']
self.filteredit.edit_text = p_data['filterexpr']
self.relevantradio.set_state(not p_data['show_all'])
self.allradio.set_state(p_data['show_all'])
def reset(self):
""" Resets the form. """
self.titleedit.set_edit_text("")
self.sortedit.set_edit_text("")
self.filteredit.set_edit_text("")
self.relevantradio.set_state(True)
self.pile.focus_item = 0
def close(self):
urwid.emit_signal(self, 'close')
def keypress(self, p_size, p_key):
if p_key == 'esc':
self.close()
else:
return super().keypress(p_size, p_key) # pylint: disable=E1102
...@@ -20,8 +20,8 @@ import os.path ...@@ -20,8 +20,8 @@ import os.path
import shlex import shlex
import sys import sys
from topydo.cli.CLIApplicationBase import CLIApplicationBase, error, usage from topydo.ui.CLIApplicationBase import CLIApplicationBase, error, usage
from topydo.cli.TopydoCompleter import TopydoCompleter from topydo.ui.prompt.TopydoCompleter import TopydoCompleter
from prompt_toolkit.shortcuts import prompt from prompt_toolkit.shortcuts import prompt
from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.history import InMemoryHistory
...@@ -59,7 +59,7 @@ class PromptApplication(CLIApplicationBase): ...@@ -59,7 +59,7 @@ class PromptApplication(CLIApplicationBase):
""" """
def __init__(self): def __init__(self):
super(PromptApplication, self).__init__() super().__init__()
self._process_flags() self._process_flags()
self.mtime = None self.mtime = None
...@@ -95,11 +95,21 @@ class PromptApplication(CLIApplicationBase): ...@@ -95,11 +95,21 @@ class PromptApplication(CLIApplicationBase):
completer=self.completer, completer=self.completer,
complete_while_typing=False) complete_while_typing=False)
user_input = shlex.split(user_input) user_input = shlex.split(user_input)
except (EOFError, KeyboardInterrupt): except EOFError:
sys.exit(0) sys.exit(0)
except KeyboardInterrupt:
continue
except ValueError as verr:
error('Error: ' + str(verr))
continue
mtime_after = _todotxt_mtime() 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 # refuse to perform operations such as 'del' and 'do' if the
# todo.txt file has been changed in the background. # todo.txt file has been changed in the background.
......
[all]
title = All tasks
filterexpr =
[today]
title = Due today
filterexpr = due:tod
[overdue]
title = Overdue tasks
filterexpr = due:<tod
sortexpr = desc:due
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