Commit d1e8d7d6 authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge branch 'master' into dot

parents 44906dc2 93544521
......@@ -11,6 +11,7 @@ build/
develop-eggs/
dist/
eggs/
htmlcov/
lib/
lib64/
parts/
......
sudo: false # run on new infrastructure
language: python
python:
- "2.7"
- "3.2"
- "3.3"
- "3.4"
- "3.5"
- "pypy"
- "pypy3"
matrix:
allow_failures:
- python: "3.2"
- python: "pypy"
- python: "pypy3"
install:
- "python -m pip install pip --upgrade"
- "pip install ."
- "pip install .[ical]"
- "pip install .[prompt-toolkit]"
- "pip install .[test]"
- "pip install pylint"
- "pip install codecov"
script:
- "green -vvr"
......
0.8
---
* `do -d` understands relative dates.
* Introduced `yesterday` as a relative date (abbrev. `yes`).
* `tag` command understands relative dates when setting due or t tags.
* Fix install of wheels (unnecessarily installed dependencies). Issue #79.
* Bugfix: the negation of ordinal tag filters did not work.
* Some improvements in test coverage (a.o. thanks to @mruwek).
0.7
---
A big release with many new features. Many thanks to Jacek Sowiński (@mruwek)
for the majority of these new features.
* `ls` output can be customized with a -F flag or a configuration option:
[ls]
list_format = |%I| %x %{(}p{)} %c %s %k %{due:}d %{t:}t
or `ls -F "%{(}p{)} %s %{due:}d"`.
See `help ls` for all placeholders. Each placeholder can optionally be
surrounded by optional texts that are only printed when the placeholder is
expanded to a value.
The format string may contain a tab character: all text that follows is
aligned to the right.
(thanks to @mruwek)
* New subcommand: `revert`. Revert the last executed command(s). The number of
revisions can be tuned in the configuration file:
[topydo]
backup_count = 25
Set to 0 to disable this feature. (thanks to @mruwek)
* New feature: aliases. Aliases can be defined in the configuration file:
[aliases]
......@@ -23,12 +57,15 @@
ls -n 5
* `ls` has a `-i` flag to select displaying todo items based on their ID. This
can be useful to have a 'clean' default view, and to gather more details for
a certain todo item using aliases and formatting.
* Prompt mode no longer warns about background modifications to todo.txt when a
read-only command is entered (e.g. `ls`).
* Removed restriction in `edit` mode that requires keeping the same amount of
lines in the todo.txt file.
* `edit` only processes the todo items when edits were actually made in the
editor.
editor (thanks to @mruwek)
* When entering today's day of the week as a relative date, it will use next
week's date instead of today.
* Bugfix: not all tags were properly hidden with the `hide_tags` configuration
......
......@@ -31,6 +31,31 @@ smoothly into topydo.
Obviously, I won't accept anything that makes the tests fail. When you submit
a Pull Request, Travis CI will automatically run all tests for various Python
versions, but it's better if you run the tests locally first.
* Travis CI will also run `pylint` and fail when new errors are introduced. You
may want to add a `pre-push` script to your topydo clone before pushing to
Github (.git/hooks/pre-push):
#!/bin/sh
remote="$1"
if [ $remote = "origin" ]; then
if ! green; then
exit 1
fi
if ! python2 -m pylint --errors-only topydo test; then
exit 1
fi
if ! python3 -m pylint --errors-only topydo test; then
exit 1
fi
fi
exit 0
Make sure to run `chmod +x .git/hooks/pre-push` to activate the hook.
* Add tests for your change(s):
* Bugfixes: add a test case that covers your bugfix, so the bug won't happen
ever again.
......
[bdist_wheel]
universal = 1
......@@ -20,7 +20,6 @@ def find_version(*file_paths):
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
setup(
name = "topydo",
packages = find_packages(exclude=["test"]),
......@@ -30,16 +29,13 @@ setup(
author_email = "me@bramschoenmakers.nl",
url = "https://github.com/bram85/topydo",
install_requires = [
'six >= 1.9.0',
'arrow >= 0.7.0',
],
extras_require = {
':sys_platform=="win32"': ['colorama>=0.2.5'],
':python_version=="2.7"': ['ushlex'],
'ical': ['icalendar'],
'prompt-toolkit': ['prompt-toolkit >= 0.53'],
'test': ['freezegun', 'coverage', 'green'],
'test:python_version=="2.7"': ['mock'],
'test:python_version!="3.2"': ['pylint'],
'test': ['coverage', 'freezegun', 'green', ],
},
entry_points= {
'console_scripts': ['topydo = topydo.cli.UILoader:main'],
......@@ -50,10 +46,12 @@ setup(
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Natural Language :: English",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Utilities",
],
long_description = """\
......
[{"completed": false, "completion_date": null, "contexts": ["Context2"], "creation_date": null, "priority": "C", "projects": ["Project1"], "source": "(C) Foo @Context2 Not@Context +Project1 Not+Project", "tags": [], "text": "Foo @Context2 Not@Context +Project1 Not+Project"}, {"completed": false, "completion_date": null, "contexts": [], "creation_date": null, "priority": "C", "projects": [], "source": "(C) Drink beer @ home", "tags": [], "text": "Drink beer @ home"}, {"completed": false, "completion_date": null, "contexts": [], "creation_date": null, "priority": "C", "projects": [], "source": "(C) 13 + 29 = 42", "tags": [], "text": "13 + 29 = 42"}, {"completed": false, "completion_date": null, "contexts": ["Context1"], "creation_date": null, "priority": "D", "projects": ["Project2"], "source": "(D) Bar @Context1 +Project2 p:1", "tags": [["p", "1"]], "text": "Bar @Context1 +Project2"}]
[{"completed": false, "completion_date": null, "contexts": ["Context2"], "creation_date": "2015-11-05", "priority": "C", "projects": ["Project1"], "source": "(C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project", "tags": [], "text": "Foo @Context2 Not@Context +Project1 Not+Project"}, {"completed": false, "completion_date": null, "contexts": [], "creation_date": null, "priority": "C", "projects": [], "source": "(C) Drink beer @ home", "tags": [], "text": "Drink beer @ home"}, {"completed": false, "completion_date": null, "contexts": [], "creation_date": null, "priority": "C", "projects": [], "source": "(C) 13 + 29 = 42", "tags": [], "text": "13 + 29 = 42"}, {"completed": false, "completion_date": null, "contexts": ["Context1"], "creation_date": null, "priority": "D", "projects": ["Project2"], "source": "(D) Bar @Context1 +Project2 p:1", "tags": [["p", "1"]], "text": "Bar @Context1 +Project2"}]
(C) Foo @Context2 Not@Context +Project1 Not+Project
(C) 2015-11-05 Foo @Context2 Not@Context +Project1 Not+Project
(D) Bar @Context1 +Project2 p:1
(C) Baz @Context1 +Project1 key:value id:1
(C) Drink beer @ home
......
(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
[ls]
list_format = |%I| %x %{(}p{)} %c %S\t%K
......@@ -18,8 +18,6 @@ import unittest
from datetime import date
from io import StringIO
from six import u
from test.command_testcase import CommandTest
from topydo.commands import AddCommand, ListCommand
from topydo.lib import TodoList
......@@ -298,29 +296,29 @@ class AddCommandTest(CommandTest):
self.assertEqual(self.errors, command.usage() + "\n")
def test_add_unicode(self):
command = AddCommand.AddCommand([u("Special \u25c4")], self.todolist,
command = AddCommand.AddCommand([u"Special \u25c4"], self.todolist,
self.out, self.error)
command.execute()
self.assertEqual(self.output,
u("| 1| {} Special \u25c4\n").format(self.today))
u"| 1| {} Special \u25c4\n".format(self.today))
self.assertEqual(self.errors, "")
@mock.patch("topydo.commands.AddCommand.stdin",
StringIO(u("Fo\u00f3 due:tod id:1\nB\u0105r before:1")))
StringIO(u"Fo\u00f3 due:tod id:1\nB\u0105r before:1"))
def test_add_from_stdin(self):
command = AddCommand.AddCommand(["-f", "-"], self.todolist, self.out,
self.error)
command.execute()
self.assertEqual(self.output, u("| 1| {tod} Fo\u00f3 due:{tod} id:1\n| 2| {tod} B\u0105r p:1\n".format(tod=self.today)))
self.assertEqual(self.output, u"| 1| {tod} Fo\u00f3 due:{tod} id:1\n| 2| {tod} B\u0105r p:1\n".format(tod=self.today))
self.assertEqual(self.errors, "")
def test_add_from_file(self):
command = AddCommand.AddCommand(["-f", "test/data/AddCommandTest-from_file.txt"], self.todolist, self.out, self.error)
command.execute()
self.assertEqual(self.output, u("| 1| {tod} Foo @fo\u00f3b\u0105r due:{tod} id:1\n| 2| {tod} Bar +baz t:{tod} p:1\n".format(tod=self.today)))
self.assertEqual(self.output, u"| 1| {tod} Foo @fo\u00f3b\u0105r due:{tod} id:1\n| 2| {tod} Bar +baz t:{tod} p:1\n".format(tod=self.today))
self.assertEqual(self.errors, "")
def test_add_task_without_date(self):
......@@ -334,6 +332,17 @@ class AddCommandTest(CommandTest):
self.assertEqual(self.todolist.todo(1).source(), "New todo")
self.assertEqual(self.errors, "")
def test_add_completed(self):
""" Add a command that is completed automatically. """
command = AddCommand.AddCommand(["x 2015-01-01 Already completed"],
self.todolist, self.out, self.error)
command.execute()
self.assertTrue(self.todolist.todo(1).is_completed())
self.assertEqual(self.output,
"| 1| x 2015-01-01 {} Already completed\n".format(self.today))
self.assertEqual(self.errors, "")
def test_help(self):
command = AddCommand.AddCommand(["help"], self.todolist, self.out,
self.error)
......
......@@ -16,8 +16,6 @@
import unittest
from six import u
from test.command_testcase import CommandTest
from topydo.commands.DeleteCommand import DeleteCommand
from topydo.lib.Config import config
......@@ -179,14 +177,14 @@ class DeleteCommandTest(CommandTest):
"""
Throw an error with invalid argument containing special characters.
"""
command = DeleteCommand([u("Fo\u00d3B\u0105r"), "Bar"], self.todolist,
command = DeleteCommand([u"Fo\u00d3B\u0105r", "Bar"], self.todolist,
self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.output, "")
self.assertEqual(self.errors,
u("Invalid todo number given: Fo\u00d3B\u0105r.\n"))
u"Invalid todo number given: Fo\u00d3B\u0105r.\n")
def test_expr_del1(self):
command = DeleteCommand(["-e", "@test"], self.todolist, self.out,
......
......@@ -16,8 +16,6 @@
import unittest
from six import u
from test.command_testcase import CommandTest
from topydo.commands.DepriCommand import DepriCommand
from topydo.lib.TodoList import TodoList
......@@ -152,14 +150,14 @@ class DepriCommandTest(CommandTest):
"""
Throw an error with invalid argument containing special characters.
"""
command = DepriCommand([u("Fo\u00d3B\u0105r"), "Bar"], self.todolist,
command = DepriCommand([u"Fo\u00d3B\u0105r", "Bar"], self.todolist,
self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertFalse(self.output)
self.assertEqual(self.errors,
u("Invalid todo number given: Fo\u00d3B\u0105r.\n"))
u"Invalid todo number given: Fo\u00d3B\u0105r.\n")
def test_empty(self):
command = DepriCommand([], self.todolist, self.out, self.error)
......
......@@ -17,8 +17,6 @@
import unittest
from datetime import date, timedelta
from six import u
from test.command_testcase import CommandTest
from topydo.commands.DoCommand import DoCommand
from topydo.lib.TodoList import TodoList
......@@ -304,6 +302,32 @@ class DoCommandTest(CommandTest):
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, "")
def test_do_custom_date8(self):
"""
Convert relative completion dates to an absolute date (yesterday).
"""
command = DoCommand(["-d", "yesterday", "3"], self.todolist, self.out,
self.error)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output,
"Completed: x {} Baz p:1\n".format(self.yesterday))
self.assertEqual(self.errors, "")
def test_do_custom_date9(self):
"""
Convert relative completion dates to an absolute date (-1d)
"""
command = DoCommand(["-d", "-1d", "3"], self.todolist, self.out,
self.error)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output,
"Completed: x {} Baz p:1\n".format(self.yesterday))
self.assertEqual(self.errors, "")
def test_multi_do1(self):
command = DoCommand(["1", "3"], self.todolist, self.out, self.error,
_yes_prompt)
......@@ -357,13 +381,13 @@ class DoCommandTest(CommandTest):
"""
Throw an error with invalid argument containing special characters.
"""
command = DoCommand([u("Fo\u00d3B\u0105r"), "Bar"], self.todolist,
command = DoCommand([u"Fo\u00d3B\u0105r", "Bar"], self.todolist,
self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.errors,
u("Invalid todo number given: Fo\u00d3B\u0105r.\n"))
u"Invalid todo number given: Fo\u00d3B\u0105r.\n")
def test_expr_do1(self):
command = DoCommand(["-e", "@test"], self.todolist, self.out,
......
......@@ -17,8 +17,6 @@
import os
import unittest
from six import u
from test.command_testcase import CommandTest
from topydo.commands.EditCommand import EditCommand
from topydo.lib.Config import config
......@@ -40,7 +38,7 @@ class EditCommandTest(CommandTest):
"Foo id:1",
"Bar p:1 @test",
"Baz @test",
u("Fo\u00f3B\u0105\u017a"),
u"Fo\u00f3B\u0105\u017a",
]
self.todolist = TodoList(todos)
......@@ -59,7 +57,7 @@ class EditCommandTest(CommandTest):
self.assertEqual(self.errors, "")
self.assertTrue(self.todolist.is_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.EditCommand._todos_from_temp')
......@@ -76,7 +74,7 @@ class EditCommandTest(CommandTest):
self.assertTrue(self.todolist.is_dirty())
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")
def test_edit03(self):
""" Throw an error after invalid todo number given as argument. """
......@@ -100,13 +98,13 @@ class EditCommandTest(CommandTest):
"""
Throw an error with invalid argument containing special characters.
"""
command = EditCommand([u("Fo\u00d3B\u0105r"), "Bar"], self.todolist,
command = EditCommand([u"Fo\u00d3B\u0105r", "Bar"], self.todolist,
self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.errors,
u("Invalid todo number given: Fo\u00d3B\u0105r.\n"))
u"Invalid todo number given: Fo\u00d3B\u0105r.\n")
@mock.patch('topydo.commands.EditCommand._is_edited')
@mock.patch('topydo.commands.EditCommand.EditCommand._todos_from_temp')
......@@ -117,14 +115,14 @@ class EditCommandTest(CommandTest):
mock_todos_from_temp.return_value = [Todo('Lazy Cat')]
mock_is_edited.return_value = True
command = EditCommand([u("Fo\u00f3B\u0105\u017a")], self.todolist,
command = EditCommand([u"Fo\u00f3B\u0105\u017a"], self.todolist,
self.out, self.error, None)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.errors, "")
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")
@mock.patch('topydo.commands.EditCommand._is_edited')
@mock.patch('topydo.commands.EditCommand.EditCommand._todos_from_temp')
......@@ -141,7 +139,7 @@ class EditCommandTest(CommandTest):
self.assertFalse(self.todolist.is_dirty())
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")
@mock.patch('topydo.commands.EditCommand._is_edited')
@mock.patch('topydo.commands.EditCommand.EditCommand._todos_from_temp')
......@@ -157,12 +155,12 @@ class EditCommandTest(CommandTest):
self.error, None)
command.execute()
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.assertTrue(self.todolist.is_dirty())
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")
@mock.patch('topydo.commands.EditCommand.check_call')
def test_edit_archive(self, mock_call):
......
......@@ -16,8 +16,6 @@
import unittest
from six import u
from test.topydo_testcase import TopydoTest
from topydo.Commands import get_subcommand
from topydo.commands.AddCommand import AddCommand
......@@ -60,7 +58,7 @@ class GetSubcommandTest(TopydoTest):
args = ["smile"]
real_cmd, final_args = get_subcommand(args)
self.assertTrue(issubclass(real_cmd, ListCommand))
self.assertEqual(final_args, [u("\u263b")])
self.assertEqual(final_args, [u"\u263b"])
def test_default_cmd01(self):
args = ["bar"]
......
This diff is collapsed.
This diff is collapsed.
......@@ -17,8 +17,6 @@
import unittest
from datetime import date, timedelta
from six import u
from test.command_testcase import CommandTest
from topydo.commands.PostponeCommand import PostponeCommand
from topydo.lib.TodoList import TodoList
......@@ -252,14 +250,14 @@ class PostponeCommandTest(CommandTest):
def test_postpone20(self):
""" Throw an error with invalid argument containing special characters. """
command = PostponeCommand([u("Fo\u00d3B\u0105r"), "Bar", "1d"],
command = PostponeCommand([u"Fo\u00d3B\u0105r", "Bar", "1d"],
self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.output, "")
self.assertEqual(self.errors,
u("Invalid todo number given: Fo\u00d3B\u0105r.\n"))
u"Invalid todo number given: Fo\u00d3B\u0105r.\n")
def test_expr_postpone1(self):
command = PostponeCommand(["-e", "due:tod", "2w"], self.todolist,
......
......@@ -16,8 +16,6 @@
import unittest
from six import u
from test.command_testcase import CommandTest
from topydo.commands.PriorityCommand import PriorityCommand
from topydo.lib.TodoList import TodoList
......@@ -198,14 +196,14 @@ class PriorityCommandTest(CommandTest):
"""
Throw an error with invalid argument containing special characters.
"""
command = PriorityCommand([u("Fo\u00d3B\u0105r"), "Bar", "C"],
command = PriorityCommand([u"Fo\u00d3B\u0105r", "Bar", "C"],
self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.output, "")
self.assertEqual(self.errors,
u("Invalid todo number given: Fo\u00d3B\u0105r.\n"))
u"Invalid todo number given: Fo\u00d3B\u0105r.\n")
def test_invalid8(self):
"""
......
......@@ -26,6 +26,7 @@ from topydo.lib.RelativeDate import relative_date_to_date
class RelativeDateTester(TopydoTest):
def setUp(self):
super(RelativeDateTester, self).setUp()
self.yesterday = date(2015, 11, 5)
self.today = date(2015, 11, 6)
self.tomorrow = date(2015, 11, 7)
self.monday = date(2015, 11, 9)
......@@ -115,6 +116,14 @@ class RelativeDateTester(TopydoTest):
result = relative_date_to_date('tom')
self.assertEqual(result, self.tomorrow)
def test_yesterday1(self):
result = relative_date_to_date('yesterday')
self.assertEqual(result, self.yesterday)
def test_yesterday2(self):
result = relative_date_to_date('yes')
self.assertEqual(result, self.yesterday)
def test_monday1(self):
result = relative_date_to_date('monday')
self.assertEqual(result, self.monday)
......
......@@ -20,7 +20,6 @@ import unittest
from datetime import date
from glob import glob
from six import u
from uuid import uuid4
from test.command_testcase import CommandTest
......
......@@ -14,6 +14,7 @@
# 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.command_testcase import CommandTest
......@@ -138,6 +139,29 @@ class TagCommandTest(CommandTest):
"| 4| Fnord due:2014-10-20 due:2014-10-20\n")
self.assertEqual(self.errors, "")
@freeze_time('2015, 11, 19')
def test_set_tag11(self):
command = TagCommand(["3", "due", "today"], self.todolist, self.out,
self.error)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "| 3| Baz due:2015-11-19\n")
self.assertEqual(self.errors, "")
def test_set_tag12(self):
"""
Do not convert relative dates for tags that were not configured as
start/due date.
"""
command = TagCommand(["3", "foo", "today"], self.todolist, self.out,
self.error)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "| 3| Baz due:2014-10-20 foo:today\n")
self.assertEqual(self.errors, "")
def test_rm_tag01(self):
command = TagCommand(["1", "due"], self.todolist, self.out, self.error)
command.execute()
......
......@@ -90,5 +90,29 @@ class TodoTest(TopydoTest):
todo = Todo("(C) Foo t:2014-01-01 due:2014-01-02")
self.assertEqual(todo.length(), 1)
def test_length4(self):
todo = Todo("(C) Foo)")
self.assertEqual(todo.length(), 0)
def test_length5(self):
todo = Todo("(C) 2015-11-18 Foo)")
self.assertEqual(todo.length(), 0)
def test_length6(self):
todo = Todo("(C) 2015-11-18 Foo due:2015-11-19)")
self.assertEqual(todo.length(), 1)
def test_length7(self):
todo = Todo("(C) 2015-11-18 Foo due:2015-11-18)")
self.assertEqual(todo.length(), 0)
def test_length8(self):
todo = Todo("(C) 2015-11-18 Foo t:2015-11-19 due:2015-11-20)")
self.assertEqual(todo.length(), 1)
def test_length9(self):
todo = Todo("(C) 2015-11-18 Foo due:2015-11-16)")
self.assertEqual(todo.length(), 0)
if __name__ == '__main__':
unittest.main()
......@@ -16,8 +16,6 @@
import unittest
from six import u
from test.facilities import load_file
from test.topydo_testcase import TopydoTest
......@@ -32,7 +30,7 @@ class TodoFileTest(TopydoTest):
todofile = load_file('test/data/utf-8.txt')
self.assertEqual(todofile[0].source(),
u('(C) \u25ba UTF-8 test \u25c4'))
u'(C) \u25ba UTF-8 test \u25c4')
if __name__ == '__main__':
unittest.main()
......@@ -331,6 +331,15 @@ class TodoListDependencyTester(TopydoTest):
self.assertEqual(str(self.todolist), old)
def test_remove_dep3(self):
""" Try to remove non-existing dependency. """
old = str(self.todolist)
from_todo = self.todolist.todo(4)
to_todo = self.todolist.todo(1)
self.todolist.remove_dependency(from_todo, to_todo)
self.assertEqual(str(self.todolist), old)
def test_remove_todo_check_children(self):
todo = self.todolist.todo(2)
self.todolist.delete(todo)
......@@ -365,15 +374,24 @@ class TodoListDependencyTester(TopydoTest):
class TodoListCleanDependencyTester(TopydoTest):
"""
Tests for cleaning up the graph:
* Transitive reduction
* Remove obsolete id: tags
* Remove obsolete p: tags
"""
def setUp(self):
super(TodoListCleanDependencyTester, self).setUp()
self.todolist = TodoList([])
def test_clean_dependencies1(self):
""" Clean p: tags from non-existing parent items. """
self.todolist.add("Bar p:1")
self.todolist.add("Baz p:1 id:2")
self.todolist.add("Buzz p:2")
def test_clean_dependencies(self):
self.todolist.clean_dependencies()
self.assertFalse(self.todolist.todo(1).has_tag('p'))
......@@ -381,5 +399,40 @@ class TodoListCleanDependencyTester(TopydoTest):
self.assertTrue(self.todolist.todo(2).has_tag('id', '2'))
self.assertTrue(self.todolist.todo(3).has_tag('p', '2'))
def test_clean_dependencies2(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:1 p:2")
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'))
def test_clean_dependencies3(self):
""" Clean id: tags from todo items without child todos. """
self.todolist.add("Foo id:1")
self.todolist.clean_dependencies()
self.assertFalse(self.todolist.todo(1).has_tag('id'))
def test_clean_dependencies4(self):
""" Clean p: items when siblings are still connected to parent. """
self.todolist.add("Foo id:1")
self.todolist.add("Bar p:1")
self.todolist.add("Baz p:1 id:2")
self.todolist.add("Buzz p:2 p:1")
self.todolist.clean_dependencies()
self.assertFalse(self.todolist.todo(4).has_tag('p', '1'))
self.assertTrue(self.todolist.todo(1).has_tag('id', '1'))
self.assertTrue(self.todolist.todo(2).has_tag('p', '1'))
if __name__ == '__main__':
unittest.main()
......@@ -16,6 +16,7 @@ auto_creation_date = 1
hide_tags = id,p,ical
indent = 0
list_limit = -1
list_format = |%I| %x %{(}p{)} %c %s %k %{due:}d %{t:}t
[tags]
tag_start = t
......@@ -51,6 +52,7 @@ append_parent_contexts = 0
[aliases]
;showall = ls -x
;next = ls -n 1
;top = ls -F '|%I| %x %p %S %k %{(}H{)}'
;lsproj = lsprj
;listprj = lsprj
;listproj = lsprj
......
......@@ -21,8 +21,6 @@ I/O on the command-line.
import getopt
import sys
from six import PY2
from six.moves import input
MAIN_OPTS = "ac:d:ht:v"
READ_ONLY_COMMANDS = ('List', 'ListContext', 'ListProject')
......@@ -128,9 +126,6 @@ class CLIApplicationBase(object):
def _process_flags(self):
args = sys.argv[1:]
if PY2:
args = [arg.decode('utf-8') for arg in args]
try:
opts, args = getopt.getopt(args, MAIN_OPTS)
except getopt.GetoptError as e:
......@@ -188,12 +183,6 @@ class CLIApplicationBase(object):
else:
pass # TODO
def _input(self):
"""
Returns a function that retrieves user input.
"""
return input
def is_read_only(self, p_command):
""" Returns True when the given command class is read-only. """
read_only_commands = tuple(cmd + 'Command' for cmd in ('Revert', ) +
......@@ -216,7 +205,7 @@ class CLIApplicationBase(object):
self.todolist,
lambda o: write(sys.stdout, o),
error,
self._input())
input)
if command.execute() != False:
return True
......
......@@ -17,6 +17,7 @@
""" Entry file for the topydo Prompt interface (CLI). """
import os.path
import shlex
import sys
from topydo.cli.CLIApplicationBase import CLIApplicationBase, error, usage
......@@ -79,8 +80,6 @@ class PromptApplication(CLIApplicationBase):
self.todolist = TodoList.TodoList(self.todofile.read())
self.mtime = current_mtime
# suppress upstream issue with Python 2.7
# pylint: disable=no-value-for-parameter
self.completer = TopydoCompleter(self.todolist)
def run(self):
......@@ -94,7 +93,8 @@ class PromptApplication(CLIApplicationBase):
try:
user_input = prompt(u'topydo> ', history=history,
completer=self.completer,
complete_while_typing=False).split()
complete_while_typing=False)
user_input = shlex.split(user_input)
except (EOFError, KeyboardInterrupt):
sys.exit(0)
......
......@@ -24,13 +24,13 @@ from sys import stdin
from topydo.lib.Command import Command
from topydo.lib.Config import config
from topydo.lib.PrettyPrinterFilter 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):
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, # pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
......
......@@ -15,17 +15,17 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.Command import Command, InvalidCommandArgument
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers
from topydo.lib.TodoListBase import InvalidTodoException
class AppendCommand(Command):
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(AppendCommand, self).__init__(
p_args, p_todolist, p_out, p_err, p_prompt=lambda a: None)
super(AppendCommand, self).__init__(p_args, p_todolist, p_out, p_err,
p_prompt)
def execute(self):
if not super(AppendCommand, self).execute():
......
......@@ -18,7 +18,7 @@ from topydo.lib.DCommand import DCommand
class DeleteCommand(DCommand):
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
......
......@@ -25,7 +25,7 @@ from topydo.lib.View import View
class DepCommand(Command):
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
......
......@@ -15,11 +15,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers
class DepriCommand(MultiCommand):
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
......
......@@ -18,13 +18,14 @@ from datetime import date
from topydo.lib.DCommand import DCommand
from topydo.lib.PrettyPrinter import PrettyPrinter
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers
from topydo.lib.Recurrence import NoRecurrenceException, advance_recurring_todo
from topydo.lib.RelativeDate import relative_date_to_date
from topydo.lib.Utils import date_string_to_date
class DoCommand(DCommand):
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
......@@ -48,7 +49,10 @@ class DoCommand(DCommand):
self.strict_recurrence = True
elif p_opt == "-d" or p_opt == "--date":
try:
self.completion_date = date_string_to_date(p_value)
self.completion_date = relative_date_to_date(p_value)
if not self.completion_date:
self.completion_date = date_string_to_date(p_value)
except ValueError:
self.completion_date = date.today()
......
......@@ -18,11 +18,9 @@ import os
import tempfile
from subprocess import CalledProcessError, check_call
from six import u
from topydo.lib.Config import config
from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers
from topydo.lib.Todo import Todo
from topydo.lib.TodoList import TodoList
......@@ -97,7 +95,7 @@ class EditCommand(MultiCommand):
if len(self.invalid_numbers) > 1 or len(self.invalid_numbers) > 0 and len(self.todos) > 0:
for number in self.invalid_numbers:
errors.append(u("Invalid todo number given: {}.").format(number))
errors.append(u"Invalid todo number given: {}.".format(number))
elif len(self.invalid_numbers) == 1 and len(self.todos) == 0:
errors.append("Invalid todo number given.")
......
......@@ -16,13 +16,14 @@
from topydo.lib.Config import config
from topydo.lib.ExpressionCommand import ExpressionCommand
from topydo.lib.Filter import InstanceFilter
from topydo.lib.PrettyPrinter import pretty_printer_factory
from topydo.lib.PrettyPrinterFilter import (PrettyPrinterHideTagFilter,
PrettyPrinterIndentFilter)
from topydo.lib.prettyprinters.Format import PrettyPrinterFormatFilter
from topydo.lib.TodoListBase import InvalidTodoException
class ListCommand(ExpressionCommand):
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
......@@ -32,6 +33,8 @@ class ListCommand(ExpressionCommand):
self.printer = None
self.sort_expression = config().sort_string()
self.show_all = False
self.ids = None
self.format = config().list_format()
def _poke_icalendar(self):
"""
......@@ -47,7 +50,7 @@ class ListCommand(ExpressionCommand):
return True
def _process_flags(self):
opts, args = self.getopt('f:n:s:x')
opts, args = self.getopt('f:F:i:n:s:x')
for opt, value in opts:
if opt == '-x':
......@@ -71,14 +74,44 @@ class ListCommand(ExpressionCommand):
self.printer = DotPrinter(self.todolist)
else:
self.printer = None
elif opt == '-F':
self.format = value
elif opt == '-n':
try:
self.limit = int(value)
except ValueError:
pass # use default value in configuration
elif opt == '-i':
self.ids = value.split(',')
# when a user requests a specific ID, it should always be shown
self.show_all = True
self.args = args
def _filters(self):
"""
Additional filters to select particular todo items given with the -i
flag.
"""
filters = super(ListCommand, self)._filters()
if self.ids:
def get_todo(p_id):
"""
Safely obtains a todo item given the user-supplied ID.
Returns None if an invalid ID was entered.
"""
try:
return self.todolist.todo(p_id)
except InvalidTodoException:
return None
todos = [get_todo(i) for i in self.ids]
filters.append(InstanceFilter(todos))
return filters
def _print(self):
"""
Prints the todos in the right format.
......@@ -90,11 +123,11 @@ class ListCommand(ExpressionCommand):
if self.printer is None:
# create a standard printer with some filters
indent = config().list_indent()
final_format = ' ' * indent + self.format
hidden_tags = config().hidden_tags()
filters = []
filters.append(PrettyPrinterIndentFilter(indent))
filters.append(PrettyPrinterHideTagFilter(hidden_tags))
filters.append(PrettyPrinterFormatFilter(self.todolist, final_format))
self.printer = pretty_printer_factory(self.todolist, filters)
......@@ -115,7 +148,8 @@ class ListCommand(ExpressionCommand):
return True
def usage(self):
return """ Synopsis: ls [-x] [-s <sort_expression>] [-f <format>] [expression]"""
return """Synopsis: ls [-x] [-s <sort_expression>] [-f <output format>]
[-F <format string>] [expression]"""
def help(self):
return """\
......@@ -137,6 +171,37 @@ 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
archived.
* 'json' - Javascript Object Notation (JSON)
-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
attribute does not exist, then it expands to an empty string.
%c: Absolute creation date.
%C: Relative creation date.
%d: Absolute due date.
%D: Relative due date.
%h: Relative due and start date (due in 3 days, started 3 days ago)
%H: Like %h with creation date.
%i: Todo number.
%I: Todo number padded with spaces (always 3 characters wide).
%k: List of tags separated by spaces (excluding hidden tags).
%K: List of all tags separated by spaces.
%p: Priority.
%s: Todo text.
%S: Todo text, truncated such that an item fits on one line.
%t: Absolute creation date.
%T: Relative creation date.
%x: 'x' followed by absolute completion date.
%X: 'x' followed by relative completion date.
\%: Literal percent sign.
Conditional characters can be added with blocks surrounded by curly
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 ''
(empty string) when an item has no priority set.
A tab character serves as a marker to start right alignment.
-i : Comma separated list of todo IDs to print.
-s : Sort the list according to a sort expression. Defaults to the expression
in the configuration.
-x : Show all todos (i.e. do not filter on dependencies or relevance).
......
......@@ -18,7 +18,7 @@ from topydo.lib.Command import Command
class ListContextCommand(Command):
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
......
......@@ -18,7 +18,7 @@ from topydo.lib.Command import Command
class ListProjectCommand(Command):
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
......
......@@ -18,13 +18,13 @@ from datetime import date, timedelta
from topydo.lib.Config import config
from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers
from topydo.lib.RelativeDate import relative_date_to_date
from topydo.lib.Utils import date_string_to_date
class PostponeCommand(MultiCommand):
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
......
......@@ -17,12 +17,12 @@
import re
from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers
from topydo.lib.Utils import is_valid_priority
class PriorityCommand(MultiCommand):
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
......
......@@ -21,12 +21,12 @@ from topydo.lib import TodoList
from topydo.lib.Config import config
class RevertCommand(Command):
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(RevertCommand, self).__init__(
p_args, p_todolist, p_out, p_err, p_prompt=lambda a: None)
super(RevertCommand, self).__init__(p_args, p_todolist, p_out, p_err,
p_prompt)
def execute(self):
if not super(RevertCommand, self).execute():
......
......@@ -20,7 +20,7 @@ from topydo.lib.Sorter import Sorter
class SortCommand(Command):
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
......
......@@ -15,12 +15,14 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.lib.Command import Command, InvalidCommandArgument
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.Config import config
from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers
from topydo.lib.RelativeDate import relative_date_to_date
from topydo.lib.TodoListBase import InvalidTodoException
class TagCommand(Command):
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
......@@ -90,7 +92,16 @@ class TagCommand(Command):
return answer
def _convert_relative_dates(self):
if self.tag == config().tag_start() or self.tag == config().tag_due():
real_date = relative_date_to_date(self.value)
if real_date:
self.value = real_date.isoformat()
def _set_helper(self, p_old_value=""):
self._convert_relative_dates()
old_src = self.todo.source()
self.todo.set_tag(self.tag, self.value, self.force_add, p_old_value)
......
......@@ -24,7 +24,7 @@ class InvalidCommandArgument(Exception):
class Command(object):
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
......
......@@ -14,16 +14,10 @@
# 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
import os
import shlex
from six import iteritems, PY2
from six.moves import configparser
if PY2:
import ushlex as shlex
import codecs
class ConfigError(Exception):
def __init__(self, p_text):
self.text = p_text
......@@ -74,6 +68,7 @@ class _Config:
'hide_tags': 'id,p,ical',
'indent': '0',
'list_limit': '-1',
'list_format': '|%I| %x %{(}p{)} %c %s %k %{due:}d %{t:}t',
},
'tags': {
......@@ -115,12 +110,12 @@ class _Config:
self.config = {}
self.cp = configparser.ConfigParser()
self.cp = configparser.RawConfigParser()
for section in self.defaults:
self.cp.add_section(section)
for option, value in iteritems(self.defaults[section]):
for option, value in self.defaults[section].items():
self.cp.set(section, option, value)
files = [
......@@ -136,16 +131,7 @@ class _Config:
if p_path is not None:
files = [p_path]
if PY2:
for path in files:
try:
with codecs.open(path, 'r', encoding='utf-8') as f:
self.cp.readfp(f)
except IOError:
pass
else:
self.cp.read(files)
self.cp.read(files)
self._supplement_sections()
if p_overrides:
......@@ -320,6 +306,11 @@ class _Config:
return alias_dict
def list_format(self):
""" Returns the list format used by `ls` """
return self.cp.get('ls', 'list_format')
def config(p_path=None, p_overrides=None):
"""
Retrieve the config instance.
......
......@@ -18,7 +18,7 @@ import re
from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.PrettyPrinter import PrettyPrinter
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers
class DCommand(MultiCommand):
......@@ -27,7 +27,7 @@ class DCommand(MultiCommand):
alike.
"""
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
......
......@@ -28,7 +28,7 @@ class ExpressionCommand(Command):
A common class for commands operating on todos selected by expressions.
"""
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
......@@ -54,17 +54,20 @@ class ExpressionCommand(Command):
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)
elif len(arg) > 1 and arg[0] == '-':
# when a word starts with -, exclude it
argfilter = Filter.GrepFilter(arg[1:])
argfilter = Filter.NegationFilter(argfilter)
else:
argfilter = Filter.GrepFilter(arg)
if is_negated:
argfilter = Filter.NegationFilter(argfilter)
result.append(argfilter)
return result
......
......@@ -185,10 +185,10 @@ class DirectedGraph(object):
childpairs = \
[(c1, c2) for c1 in neighbors for c2 in neighbors if c1 != c2]
for pair in childpairs:
if self.has_path(pair[0], pair[1]) \
and not self.has_path(pair[0], from_node):
removals.add((from_node, pair[1]))
for child1, child2 in childpairs:
if self.has_path(child1, child2) \
and not self.has_path(child1, from_node):
removals.add((from_node, child2))
for edge in removals:
self.remove_edge(edge[0], edge[1])
......
......@@ -45,7 +45,7 @@ def _to_base36(p_value):
return base36 or alphabet[0]
def hash_list_values(p_list, p_key=lambda i: i):
def hash_list_values(p_list, p_key=lambda i: i): # pragma: no branch
"""
Calculates a unique value for each item in the list, these can be used as
identifiers.
......
This diff is collapsed.
......@@ -14,8 +14,6 @@
# 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 six import u
from topydo.lib.ExpressionCommand import ExpressionCommand
from topydo.lib.TodoListBase import InvalidTodoException
......@@ -25,7 +23,7 @@ class MultiCommand(ExpressionCommand):
A common class for operations that can work with multiple todo IDs.
"""
def __init__(self, p_args, p_todolist,
def __init__(self, p_args, p_todolist, #pragma: no branch
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
......@@ -90,7 +88,7 @@ class MultiCommand(ExpressionCommand):
if len(self.invalid_numbers) > 1 or len(self.invalid_numbers) > 0 and len(self.todos) > 0:
for number in self.invalid_numbers:
errors.append(u("Invalid todo number given: {}.").format(number))
errors.append(u"Invalid todo number given: {}.".format(number))
elif len(self.invalid_numbers) == 1 and len(self.todos) == 0:
errors.append("Invalid todo number given.")
elif len(self.todos) == 0 and len(self.invalid_numbers) == 0:
......@@ -106,14 +104,14 @@ class MultiCommand(ExpressionCommand):
Operations specific for particular command dealing with multiple todo
IDs.
"""
pass
raise NotImplementedError
def _execute_not_multi(self):
"""
Some commands can do something else besides operating on multiple todo
IDs. This method is a wrapper for those other operations.
"""
pass
raise NotImplementedError
def execute(self):
if not super(MultiCommand, self).execute():
......
......@@ -14,8 +14,8 @@
# 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.PrettyPrinterFilter import (PrettyPrinterColorFilter,
PrettyPrinterNumbers)
from topydo.lib.prettyprinters.Colors import PrettyPrinterColorFilter
from topydo.lib.prettyprinters.Numbers import PrettyPrinterNumbers
class Printer(object):
......
......@@ -14,14 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Provides filters used for pretty printing. """
import re
from six import u
from topydo.lib.Colors import NEUTRAL_COLOR, Colors
from topydo.lib.Config import config
""" Provides a base class for pretty printer filters. """
class PrettyPrinterFilter(object):
......@@ -37,91 +30,3 @@ class PrettyPrinterFilter(object):
"""
raise NotImplementedError
class PrettyPrinterColorFilter(PrettyPrinterFilter):
"""
Adds colors to the todo string by inserting ANSI codes.
Should be passed as a filter in the filter list of pretty_print()
"""
def filter(self, p_todo_str, p_todo):
""" Applies the colors. """
colorscheme = Colors()
priority_colors = colorscheme.get_priority_colors()
project_color = colorscheme.get_project_color()
context_color = colorscheme.get_context_color()
metadata_color = colorscheme.get_metadata_color()
link_color = colorscheme.get_link_color()
if config().colors():
color = NEUTRAL_COLOR
try:
color = priority_colors[p_todo.priority()]
except KeyError:
pass
# color by priority
p_todo_str = color + p_todo_str
# color projects / contexts
p_todo_str = re.sub(
r'\B(\+|@)(\S*\w)',
lambda m: (
context_color if m.group(0)[0] == "@"
else project_color) + m.group(0) + color,
p_todo_str)
# tags
p_todo_str = re.sub(r'\b\S+:[^/\s]\S*\b',
metadata_color + r'\g<0>' + color,
p_todo_str)
# add link_color to any valid URL specified outside of the tag.
p_todo_str = re.sub(r'(^|\s)(\w+:){1}(//\S+)',
' ' + link_color + r'\2\3' + color,
p_todo_str)
p_todo_str += NEUTRAL_COLOR
return p_todo_str
class PrettyPrinterIndentFilter(PrettyPrinterFilter):
""" Adds indentation to the todo item. """
def __init__(self, p_indent=0):
super(PrettyPrinterIndentFilter, self).__init__()
self.indent = p_indent
def filter(self, p_todo_str, _):
""" Applies the indentation. """
return ' ' * self.indent + p_todo_str
class PrettyPrinterNumbers(PrettyPrinterFilter):
""" Prepends the todo's number, retrieved from the todolist. """
def __init__(self, p_todolist):
super(PrettyPrinterNumbers, self).__init__()
self.todolist = p_todolist
def filter(self, p_todo_str, p_todo):
""" Prepends the number to the todo string. """
return u("|{:>3}| {}").format(self.todolist.number(p_todo), p_todo_str)
class PrettyPrinterHideTagFilter(PrettyPrinterFilter):
""" Removes all occurrences of the given tags from the text. """
def __init__(self, p_hidden_tags):
super(PrettyPrinterHideTagFilter, self).__init__()
self.hidden_tags = p_hidden_tags
def filter(self, p_todo_str, _):
for hidden_tag in self.hidden_tags:
# inspired from remove_tag in TodoBase
p_todo_str = re.sub(r'\s?\b' + hidden_tag + r':\S+\b', '',
p_todo_str)
return p_todo_str
......@@ -91,7 +91,7 @@ def relative_date_to_date(p_date, p_offset=None):
The following formats are understood:
* [0-9][dwmy]
* 'today' or 'tomorrow'
* 'yesterday', 'today' or 'tomorrow'
* days of the week (in full or abbreviated)
"""
result = None
......@@ -126,4 +126,7 @@ def relative_date_to_date(p_date, p_offset=None):
elif re.match('tom(orrow)?$', p_date):
result = _convert_pattern('1', 'd')
elif re.match('yes(terday)?$', p_date):
result = _convert_pattern('-1', 'd')
return result
......@@ -85,9 +85,10 @@ class Todo(TodoBase):
def length(self):
"""
Returns the length (in days) of the task, by considering the start date
and the due date. Returns 0 when one of these dates are missing.
and the due date. When there is no start date, its creation date is
used. Returns 0 when one of these dates is missing.
"""
start = self.start_date()
start = self.start_date() or self.creation_date()
due = self.due_date()
if start and due and start < due:
......
......@@ -21,8 +21,6 @@ This module contains the class that represents a single todo item.
import re
from datetime import date
from six import u
from topydo.lib.TodoParser import parse_line
from topydo.lib.Utils import is_valid_priority
......@@ -218,7 +216,7 @@ class TodoBase(object):
self.src = re.sub(
r'^(x \d{4}-\d{2}-\d{2} |\([A-Z]\) )?(\d{4}-\d{2}-\d{2} )?(.*)$',
lambda m:
u("{}{} {}").format(m.group(1) or '', p_date.isoformat(),
u"{}{} {}".format(m.group(1) or '', p_date.isoformat(),
m.group(3)), self.src)
def creation_date(self):
......
......@@ -203,19 +203,39 @@ class TodoList(TodoListBase):
graph and removing unused dependency ids from the graph (in that
order).
"""
def clean_by_tag(tag_name):
""" Generic function to handle 'p' and 'id' tags. """
for todo in [todo for todo in self._todos
if todo.has_tag(tag_name)]:
def remove_tag(p_todo, p_tag, p_value):
"""
Removes a tag from a todo item.
"""
p_todo.remove_tag(p_tag, p_value)
self.dirty = True
def clean_parent_relations():
"""
Remove id: tags for todos without child todo items.
"""
value = todo.tag_value(tag_name)
for todo in [todo for todo in self._todos if todo.has_tag('id')]:
value = todo.tag_value('id')
if not self._depgraph.has_edge_id(value):
todo.remove_tag(tag_name, value)
self.dirty = True
remove_tag(todo, 'id', value)
def clean_orphan_relations():
"""
Remove p: tags for todos referring to a parent that is not in the
dependency graph anymore.
"""
for todo in [todo for todo in self._todos if todo.has_tag('p')]:
for value in todo.tag_values('p'):
parent = self.todo_by_dep_id(value)
if not self._depgraph.has_edge(hash(parent), hash(todo)):
remove_tag(todo, 'p', value)
self._depgraph.transitively_reduce()
clean_by_tag('p')
clean_by_tag('id')
clean_parent_relations()
clean_orphan_relations()
def _update_parent_cache(self):
"""
......
......@@ -21,8 +21,6 @@ A list of todo items.
import re
from datetime import date
from six import text_type
from topydo.lib import Filter
from topydo.lib.Config import config
from topydo.lib.HashListValues import hash_list_values
......@@ -128,7 +126,7 @@ class TodoListBase(object):
if not result:
# convert integer to text so we pass on a valid regex
result = todo_by_regexp(text_type(p_identifier))
result = todo_by_regexp(str(p_identifier))
return result
......
......@@ -19,6 +19,8 @@ Various utility functions.
"""
import re
from collections import namedtuple
from datetime import date
......@@ -51,3 +53,22 @@ def escape_ansi(p_string):
return escape_ansi.pattern.sub('', p_string)
escape_ansi.pattern = re.compile(r'\x1b[^m]*m')
def get_terminal_size():
"""
Try to determine terminal size at run time. If that is not possible,
returns the default size of 80x24.
"""
from shutil import get_terminal_size # pylint: disable=no-name-in-module
try:
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
""" Version of Topydo. """
VERSION = '0.6'
VERSION = '0.8'
LICENSE = """Copyright (C) 2014-2015 Bram Schoenmakers
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
......
# 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/>.
""" Provides a pretty printer filter that colorizes todo items. """
import re
from topydo.lib.Colors import NEUTRAL_COLOR, Colors
from topydo.lib.Config import config
from topydo.lib.PrettyPrinterFilter import PrettyPrinterFilter
class PrettyPrinterColorFilter(PrettyPrinterFilter):
"""
Adds colors to the todo string by inserting ANSI codes.
Should be passed as a filter in the filter list of pretty_print()
"""
def filter(self, p_todo_str, p_todo):
""" Applies the colors. """
if config().colors():
colorscheme = Colors()
priority_colors = colorscheme.get_priority_colors()
project_color = colorscheme.get_project_color()
context_color = colorscheme.get_context_color()
metadata_color = colorscheme.get_metadata_color()
link_color = colorscheme.get_link_color()
priority_color = NEUTRAL_COLOR
try:
priority_color = priority_colors[p_todo.priority()]
except KeyError:
pass
# color projects / contexts
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
p_todo_str = re.sub(r'\b\S+:[^/\s]\S*\b',
metadata_color + r'\g<0>' + priority_color,
p_todo_str)
# add link_color to any valid URL specified outside of the tag.
p_todo_str = re.sub(r'(^|\s)(\w+:){1}(//\S+)',
r'\1' + link_color + r'\2\3' + priority_color,
p_todo_str)
p_todo_str += NEUTRAL_COLOR
# color by priority
p_todo_str = priority_color + p_todo_str
return p_todo_str
# 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/>.
"""
Provides a pretty printer filter that generates a todo string based on a format
string.
"""
from topydo.lib.PrettyPrinterFilter import PrettyPrinterFilter
from topydo.lib.ListFormat import ListFormatParser
class PrettyPrinterFormatFilter(PrettyPrinterFilter):
def __init__(self, p_todolist, p_format=None):
super(PrettyPrinterFormatFilter, self).__init__()
self.parser = ListFormatParser(p_todolist, p_format)
def filter(self, p_todo_str, p_todo):
p_todo_str = self.parser.parse(p_todo)
return p_todo_str
# 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/>.
""" Provides a pretty printer filter that inserts todo numbers. """
from topydo.lib.PrettyPrinterFilter import PrettyPrinterFilter
class PrettyPrinterNumbers(PrettyPrinterFilter):
""" Prepends the todo's number, retrieved from the todolist. """
def __init__(self, p_todolist):
super(PrettyPrinterNumbers, self).__init__()
self.todolist = p_todolist
def filter(self, p_todo_str, p_todo):
""" Prepends the number to the todo string. """
return "|{:>3}| {}".format(self.todolist.number(p_todo), p_todo_str)
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