Commit 35cb964e authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge branch 'master' into column-ui/master

Conflicts:
	setup.py
	topydo/lib/ExpressionCommand.py
parents d172148f 14b8d769
[run]
source = topydo
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if 0:
if __name__ == .__main__.:
omit =
topydo/lib/ExitCommand.py
topydo/lib/Version.py
*.pyc *.pyc
*.sw? *.sw?
build
dist
install install
.coverage
# Distribution / packaging
.Python
env/
env*/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# Sublime Text
*.sublime-*
sudo: false # run on new infrastructure
language: python language: python
python: python:
- "2.7" - "2.7"
...@@ -10,6 +11,10 @@ install: ...@@ -10,6 +11,10 @@ install:
- "pip install icalendar" - "pip install icalendar"
- "pip install pylint" - "pip install pylint"
script: "./run-tests.sh" script: "./run-tests.sh"
# Cache Dependencies
cache:
directories:
- $HOME/travis/.cache/pip
notifications: notifications:
webhooks: webhooks:
urls: urls:
......
0.6
---
* Recurrence patterns can be prefixed with a `+` to indicate strict recurrence
(i.e. based on due date rather than completion date. This syntax is inspired
from the SimpleTask project by @mpcjanssen.
* Colors now work on the Windows commandline (thanks to @MinchinWeb). Requires
colorama to be installed.
* Do not print spurious color codes when colors are disabled in the
configuration (thanks to @MinchinWeb).
* In prompt mode, restore old auto-completion behavior: press Tab for
completion (instead of complete while typing).
* Various other minor fixes (thanks to @MinchinWeb).
0.5
---
* Remove 'ical' subcommand in favor of 'topydo ls -f ical'
* Remove options highlight_projects_colors in favor of colorscheme options. In case you wish to disable the project/context colors, assign an empty value in the configuration file:
[colorscheme]
project_color =
context_color =
* `del`, `depri`, `do`, `pri`, `postpone` now support now expression like `ls`
does, using the `-e` flag (Jacek Sowiński, @mruwek).
* Fix `ls` when searching for a certain key:value where value is a string.
* Disable auto archive when the option archive_filename is empty.
* Add option auto_creation_date to enable/disable the creation date being added
to new todo items.
* Calculate relative dates correctly in long-running `prompt` sessions.
* `pri` also accepts priorities in the form (A), [A] or any other bracket.
* Add `listcontext` and `listcontexts` as aliases of `lscon`.
* Highlight tags when the value is one character long.
* Cleanups
0.4.1
-----
* Fix infinite loop when `keep_sorted` is enabled in the configuration.
* Depend on prompt-toolkit >= 0.39, which fixes the history functionality in
prompt mode (up/down keys).
0.4 0.4
--- ---
......
If you're reading this, you may have interest in enhancing topydo. Thank you! If you're reading this, you may have interest in enhancing topydo. Thank you!
Please read the following guidelines to get your enhancement / bug fixes Please read the following guidelines to get your enhancement / bug fixes
smoothly into topydo: smoothly into topydo.
### General
* This Github page defaults to the **stable** branch which is for **bug fixes * This Github page defaults to the **stable** branch which is for **bug fixes
only**. If you would like to add a new feature, make sure to make a Pull only**. If you would like to add a new feature, make sure to make a Pull
Request on the `master` branch. Request on the `master` branch.
* Use descriptive commit messages.
### Coding style
* Please try to adhere to the coding style dictated by `pylint` as much
possible. I won't be very picky about long lines, but please try to avoid
them.
* I strongly prefer simple and short functions, doing only one thing. I'll
request you to refactor functions with massive indentation or don't fit
otherwise on a screen.
### Testing
* Run tests with: * Run tests with:
./run-tests.sh [python2|python3] ./run-tests.sh [python2|python3]
...@@ -21,13 +37,17 @@ smoothly into topydo: ...@@ -21,13 +37,17 @@ smoothly into topydo:
ever again. ever again.
* Features: add testcases that checks various inputs and outputs of your * Features: add testcases that checks various inputs and outputs of your
feature. Be creative in trying to break the feature you've just implemented. feature. Be creative in trying to break the feature you've just implemented.
* Use descriptive commit messages. * Check the test coverage of your contributed code, in particular if you
touched code in the topydo.lib or topydo.command packages:
### Coding style pip install coverage
coverage run setup.py test
coverage report
Or alternatively, for a more friendly output, run:
coverage html
Which will generate annotated files in the *htmlcov* folder. The new code
should be marked green (i.e. covered).
* Please try to adhere to the coding style dictated by `pylint` as much
possible. I won't be very picky about long lines, but please try to avoid
them.
* I strongly prefer simple and short functions, doing only one thing. I'll
request you to refactor functions with massive indentation or don't fit
otherwise on a screen.
topydo topydo
====== ======
[![Build Status](https://travis-ci.org/bram85/topydo.svg?branch=master)](https://travis-ci.org/bram85/topydo) [![Join the chat at https://gitter.im/bram85/topydo](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/bram85/topydo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/bram85/topydo.svg?branch=master)](https://travis-ci.org/bram85/topydo) [![Join the chat at https://gitter.im/bram85/topydo](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/bram85/topydo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Flattr this git repo](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=bram85&url=https://github.com/bram85/topydo&title=topydo&language=&tags=github&category=software)
topydo is a todo list application using the [todo.txt format][1]. It is heavily topydo is a todo list application using the [todo.txt format][1]. It is heavily
inspired by the [todo.txt CLI][2] by Gina Trapani. This tool is actually a inspired by the [todo.txt CLI][2] by Gina Trapani. This tool is actually a
...@@ -22,10 +22,17 @@ use topydo. ...@@ -22,10 +22,17 @@ use topydo.
Install Install
------- -------
Install simply with: Simply install with:
pip install topydo pip install topydo
### Optional dependencies
* icalendar : To print your todo.txt file as an iCalendar file
(not supported for Python 3.2).
* prompt-toolkit : For topydo's _prompt_ mode, which offers a shell-like
interface with auto-completion.
Demo Demo
---- ----
......
from setuptools import setup, find_packages from setuptools import setup, find_packages
import os
import re
import codecs
import sys
here = os.path.abspath(os.path.dirname(__file__))
def read(*parts):
# intentionally *not* adding an encoding option to open
return codecs.open(os.path.join(here, *parts), 'r').read()
def find_version(*file_paths):
version_file = read(*file_paths)
version_match = re.search(r"^VERSION = ['\"]([^'\"]*)['\"]",
version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
conditional_dependencies = {
"colorama>=0.2.5": "win32" in sys.platform,
}
setup( setup(
name = "topydo", name = "topydo",
packages = find_packages(exclude=["test"]), packages = find_packages(exclude=["test"]),
version = "0.4", version = find_version('topydo', 'lib', 'Version.py'),
description = "A command-line todo list application using the todo.txt format.", description = "A command-line todo list application using the todo.txt format.",
author = "Bram Schoenmakers", author = "Bram Schoenmakers",
author_email = "me@bramschoenmakers.nl", author_email = "me@bramschoenmakers.nl",
url = "https://github.com/bram85/topydo", url = "https://github.com/bram85/topydo",
install_requires = [ install_requires = [
'six >= 1.9.0', 'six >= 1.9.0',
], ] + [p for p, cond in conditional_dependencies.items() if cond],
extras_require = { extras_require = {
'ical': ['icalendar'], 'ical': ['icalendar'],
'prompt-toolkit': ['prompt-toolkit >= 0.39'], 'prompt-toolkit': ['prompt-toolkit >= 0.47'],
'urwid': ['urwid >= 1.3.0'], 'urwid': ['urwid >= 1.3.0'],
'edit-cmd-tests': ['mock'], 'edit-cmd-tests': ['mock'],
}, },
...@@ -21,7 +47,7 @@ setup( ...@@ -21,7 +47,7 @@ setup(
'console_scripts': ['topydo = topydo.cli.UILoader:main'], 'console_scripts': ['topydo = topydo.cli.UILoader:main'],
}, },
classifiers = [ classifiers = [
"Development Status :: 4 - Beta", "Development Status :: 5 - Production/Stable",
"Environment :: Console", "Environment :: Console",
"Intended Audience :: End Users/Desktop", "Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
......
...@@ -266,6 +266,16 @@ class AddCommandTest(CommandTest): ...@@ -266,6 +266,16 @@ class AddCommandTest(CommandTest):
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, "") self.assertEqual(self.errors, "")
def test_add_task_without_date(self):
config(p_overrides={('add', 'auto_creation_date'): '0'})
args = ["New todo"]
command = AddCommand.AddCommand(args, self.todolist, self.out, self.error)
command.execute()
self.assertEqual(self.todolist.todo(1).source(), "New todo")
self.assertEqual(self.errors, "")
def test_help(self): def test_help(self):
command = AddCommand.AddCommand(["help"], self.todolist, self.out, self.error) command = AddCommand.AddCommand(["help"], self.todolist, self.out, self.error)
command.execute() command.execute()
......
...@@ -35,6 +35,8 @@ class DeleteCommandTest(CommandTest): ...@@ -35,6 +35,8 @@ class DeleteCommandTest(CommandTest):
todos = [ todos = [
"Foo id:1", "Foo id:1",
"Bar p:1", "Bar p:1",
"a @test with due:2015-06-03",
"a @test with +project",
] ]
self.todolist = TodoList(todos) self.todolist = TodoList(todos)
...@@ -62,7 +64,7 @@ class DeleteCommandTest(CommandTest): ...@@ -62,7 +64,7 @@ class DeleteCommandTest(CommandTest):
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.todolist.count(), 0) self.assertEqual(self.todolist.count(), 2)
self.assertEqual(self.output, "| 2| Bar p:1\nRemoved: Bar\nRemoved: Foo\n") self.assertEqual(self.output, "| 2| Bar p:1\nRemoved: Bar\nRemoved: Foo\n")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
...@@ -71,7 +73,7 @@ class DeleteCommandTest(CommandTest): ...@@ -71,7 +73,7 @@ class DeleteCommandTest(CommandTest):
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.todolist.count(), 1) # 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, "")
...@@ -80,7 +82,7 @@ class DeleteCommandTest(CommandTest): ...@@ -80,7 +82,7 @@ class DeleteCommandTest(CommandTest):
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.todolist.count(), 1) # 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, "")
...@@ -116,7 +118,9 @@ class DeleteCommandTest(CommandTest): ...@@ -116,7 +118,9 @@ class DeleteCommandTest(CommandTest):
command = DeleteCommand(["8to"], self.todolist, self.out, self.error) command = DeleteCommand(["8to"], self.todolist, self.out, self.error)
command.execute() command.execute()
self.assertEqual(self.todolist.print_todos(), "Foo") result = "Foo\na @test with due:2015-06-03\na @test with +project"
self.assertEqual(self.todolist.print_todos(), result)
self.assertRaises(InvalidTodoException, self.todolist.todo, 'b0n') self.assertRaises(InvalidTodoException, self.todolist.todo, 'b0n')
def test_multi_del1(self): def test_multi_del1(self):
...@@ -124,14 +128,20 @@ class DeleteCommandTest(CommandTest): ...@@ -124,14 +128,20 @@ class DeleteCommandTest(CommandTest):
command = DeleteCommand(["1", "2"], self.todolist, self.out, self.error, _no_prompt) command = DeleteCommand(["1", "2"], self.todolist, self.out, self.error, _no_prompt)
command.execute() command.execute()
self.assertEqual(self.todolist.count(), 0) result = "a @test with due:2015-06-03\na @test with +project"
self.assertEqual(self.todolist.count(), 2)
self.assertEqual(self.todolist.print_todos(), result)
def test_multi_del2(self): def test_multi_del2(self):
""" Test deletion of multiple items. """ """ Test deletion of multiple items. """
command = DeleteCommand(["1", "2"], self.todolist, self.out, self.error, _yes_prompt) command = DeleteCommand(["1", "2"], self.todolist, self.out, self.error, _yes_prompt)
command.execute() command.execute()
self.assertEqual(self.todolist.count(), 0) result = "a @test with due:2015-06-03\na @test with +project"
self.assertEqual(self.todolist.count(), 2)
self.assertEqual(self.todolist.print_todos(), result)
def test_multi_del3(self): def test_multi_del3(self):
""" Fail if any of supplied todo numbers is invalid. """ """ Fail if any of supplied todo numbers is invalid. """
...@@ -160,6 +170,50 @@ class DeleteCommandTest(CommandTest): ...@@ -160,6 +170,50 @@ class DeleteCommandTest(CommandTest):
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, u("Invalid todo number given: Fo\u00d3B\u0105r.\n")) self.assertEqual(self.errors, u("Invalid todo number given: Fo\u00d3B\u0105r.\n"))
def test_expr_del1(self):
command = DeleteCommand(["-e", "@test"], self.todolist, self.out, self.error, None)
command.execute()
result = "Removed: a @test with due:2015-06-03\nRemoved: a @test with +project\n"
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.todolist.count(), 2)
self.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_expr_del2(self):
command = DeleteCommand(["-e", "@test", "due:2015-06-03"], self.todolist, self.out, self.error, None)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "Removed: a @test with due:2015-06-03\n")
self.assertEqual(self.errors, "")
def test_expr_del3(self):
command = DeleteCommand(["-e", "@test", "due:2015-06-03", "+project"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_del4(self):
""" Remove only relevant todo items. """
command = DeleteCommand(["-e", ""], self.todolist, self.out, self.error, None)
command.execute()
result = "Foo"
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.todolist.count(), 1)
self.assertEqual(self.todolist.print_todos(), result)
def test_expr_del5(self):
""" Force deleting unrelevant items with additional -x flag. """
command = DeleteCommand(["-xe", ""], self.todolist, self.out, self.error, _yes_prompt)
command.execute()
self.assertTrue(self.todolist.is_dirty())
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()
......
...@@ -211,6 +211,14 @@ class DepCommandTest(CommandTest): ...@@ -211,6 +211,14 @@ 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")
def test_ls7(self):
command = DepCommand(["ls", "top", "99"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.output, "")
self.assertEqual(self.errors, command.usage() + "\n")
def gc_helper(self, p_subcommand): def gc_helper(self, p_subcommand):
command = DepCommand([p_subcommand], self.todolist, self.out, self.error) command = DepCommand([p_subcommand], self.todolist, self.out, self.error)
command.execute() command.execute()
......
...@@ -28,6 +28,9 @@ class DepriCommandTest(CommandTest): ...@@ -28,6 +28,9 @@ class DepriCommandTest(CommandTest):
"(A) Foo", "(A) Foo",
"Bar", "Bar",
"(B) Baz", "(B) Baz",
"(E) a @test with due:2015-06-03",
"(Z) a @test with +project p:1",
"(D) Bax id:1",
] ]
self.todolist = TodoList(todos) self.todolist = TodoList(todos)
...@@ -69,6 +72,50 @@ class DepriCommandTest(CommandTest): ...@@ -69,6 +72,50 @@ class DepriCommandTest(CommandTest):
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")
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
def test_expr_depri1(self):
command = DepriCommand(["-e", "@test"], self.todolist, self.out, self.error, None)
command.execute()
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.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_expr_depri2(self):
command = DepriCommand(["-e", "@test", "due:2015-06-03"], self.todolist, self.out, self.error, None)
command.execute()
result = "Priority removed.\n| 4| a @test with due:2015-06-03\n"
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_expr_depri3(self):
command = DepriCommand(["-e", "@test", "due:2015-06-03", "+project"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_depri4(self):
""" Don't remove priority from unrelevant todo items. """
command = DepriCommand(["-e", "Bax"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_depri5(self):
""" Force unprioritizing unrelevant items with additional -x flag. """
command = DepriCommand(["-xe", "Bax"], self.todolist, self.out, self.error, None)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "Priority removed.\n| 6| Bax id:1\n")
self.assertEqual(self.errors, "")
def test_invalid1(self): def test_invalid1(self):
command = DepriCommand(["99"], self.todolist, self.out, self.error) command = DepriCommand(["99"], self.todolist, self.out, self.error)
......
...@@ -41,6 +41,8 @@ class DoCommandTest(CommandTest): ...@@ -41,6 +41,8 @@ class DoCommandTest(CommandTest):
"Subtodo of inactive p:2", "Subtodo of inactive p:2",
"Strict due:2014-01-01 rec:1d", "Strict due:2014-01-01 rec:1d",
"Invalid rec:1", "Invalid rec:1",
"a @test with due:2015-06-03",
"a @test with +project",
] ]
self.todolist = TodoList(todos) self.todolist = TodoList(todos)
...@@ -123,7 +125,7 @@ class DoCommandTest(CommandTest): ...@@ -123,7 +125,7 @@ class DoCommandTest(CommandTest):
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
self.assertEqual(self.todolist.count(), 10) self.assertEqual(self.todolist.count(), 12)
def test_recurrence(self): def test_recurrence(self):
self.assertFalse(self.todolist.todo(4).has_tag('due')) self.assertFalse(self.todolist.todo(4).has_tag('due'))
...@@ -131,7 +133,7 @@ class DoCommandTest(CommandTest): ...@@ -131,7 +133,7 @@ class DoCommandTest(CommandTest):
self._recurrence_helper(["4"]) self._recurrence_helper(["4"])
self.assertTrue(self.todolist.todo(4).is_completed()) self.assertTrue(self.todolist.todo(4).is_completed())
result = "| 10| {today} Recurring! rec:1d due:{tomorrow}\nCompleted: x {today} Recurring! rec:1d\n".format(today=self.today, tomorrow=self.tomorrow) result = "| 12| {today} Recurring! rec:1d due:{tomorrow}\nCompleted: x {today} Recurring! rec:1d\n".format(today=self.today, tomorrow=self.tomorrow)
self.assertEqual(self.output, result) self.assertEqual(self.output, result)
todo = self.todolist.todo(10) todo = self.todolist.todo(10)
...@@ -140,13 +142,13 @@ class DoCommandTest(CommandTest): ...@@ -140,13 +142,13 @@ class DoCommandTest(CommandTest):
def test_strict_recurrence1(self): def test_strict_recurrence1(self):
self._recurrence_helper(["-s", "8"]) self._recurrence_helper(["-s", "8"])
result = "| 10| {today} Strict due:2014-01-02 rec:1d\nCompleted: x {today} Strict due:2014-01-01 rec:1d\n".format(today=self.today) result = "| 12| {today} Strict due:2014-01-02 rec:1d\nCompleted: x {today} Strict due:2014-01-01 rec:1d\n".format(today=self.today)
self.assertEqual(self.output, result) self.assertEqual(self.output, result)
def test_strict_recurrence2(self): def test_strict_recurrence2(self):
self._recurrence_helper(["--strict", "8"]) self._recurrence_helper(["--strict", "8"])
result = "| 10| {today} Strict due:2014-01-02 rec:1d\nCompleted: x {today} Strict due:2014-01-01 rec:1d\n".format(today=self.today) result = "| 12| {today} Strict due:2014-01-02 rec:1d\nCompleted: x {today} Strict due:2014-01-01 rec:1d\n".format(today=self.today)
self.assertEqual(self.output, result) self.assertEqual(self.output, result)
def test_invalid1(self): def test_invalid1(self):
...@@ -254,7 +256,7 @@ class DoCommandTest(CommandTest): ...@@ -254,7 +256,7 @@ class DoCommandTest(CommandTest):
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "| 10| {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, "")
def test_do_custom_date6(self): def test_do_custom_date6(self):
...@@ -267,7 +269,7 @@ class DoCommandTest(CommandTest): ...@@ -267,7 +269,7 @@ class DoCommandTest(CommandTest):
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "| 10| {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, "")
def test_do_custom_date7(self): def test_do_custom_date7(self):
...@@ -279,7 +281,7 @@ class DoCommandTest(CommandTest): ...@@ -279,7 +281,7 @@ class DoCommandTest(CommandTest):
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "| 10| {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, "")
def test_multi_do1(self): def test_multi_do1(self):
...@@ -320,10 +322,10 @@ class DoCommandTest(CommandTest): ...@@ -320,10 +322,10 @@ class DoCommandTest(CommandTest):
""" """
Check output when all supplied todo numbers are invalid. Check output when all supplied todo numbers are invalid.
""" """
command = DoCommand(["99", "10"], self.todolist, self.out, self.error, _no_prompt) command = DoCommand(["99", "15"], self.todolist, self.out, self.error, _no_prompt)
command.execute() command.execute()
self.assertEqual(self.errors, "Invalid todo number given: 99.\nInvalid todo number given: 10.\n") self.assertEqual(self.errors, "Invalid todo number given: 99.\nInvalid todo number given: 15.\n")
def test_multi_do6(self): def test_multi_do6(self):
""" Throw an error with invalid argument containing special characters. """ """ Throw an error with invalid argument containing special characters. """
...@@ -333,6 +335,46 @@ class DoCommandTest(CommandTest): ...@@ -333,6 +335,46 @@ class DoCommandTest(CommandTest):
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.errors, u("Invalid todo number given: Fo\u00d3B\u0105r.\n")) self.assertEqual(self.errors, u("Invalid todo number given: Fo\u00d3B\u0105r.\n"))
def test_expr_do1(self):
command = DoCommand(["-e", "@test"], self.todolist, self.out, self.error, None)
command.execute()
self.assertTrue(self.todolist.is_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.errors, "")
def test_expr_do2(self):
command = DoCommand(["-e", "@test", "due:2015-06-03"], self.todolist, self.out, self.error, None)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "Completed: x {} a @test with due:2015-06-03\n".format(self.today))
self.assertEqual(self.errors, "")
def test_expr_do3(self):
command = DoCommand(["-e", "@test", "due:2015-06-03", "+project"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_do4(self):
""" Don't do anything with unrelevant todo items. """
command = DoCommand(["-e", "Foo"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_do5(self):
""" Force marking unrelevant items as done with additional -x flag. """
command = DoCommand(["-xe", "Foo"], self.todolist, self.out, self.error, _yes_prompt)
command.execute()
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.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_invalid_recurrence(self): def test_invalid_recurrence(self):
""" Show error message when an item has an invalid recurrence pattern. """ """ Show error message when an item has an invalid recurrence pattern. """
command = DoCommand(["9"], self.todolist, self.out, self.error, _no_prompt) command = DoCommand(["9"], self.todolist, self.out, self.error, _no_prompt)
......
...@@ -139,7 +139,7 @@ class EditCommandTest(CommandTest): ...@@ -139,7 +139,7 @@ class EditCommandTest(CommandTest):
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"))
@mock.patch('topydo.commands.EditCommand.call') @mock.patch('topydo.commands.EditCommand.check_call')
def test_edit_archive(self, mock_call): def test_edit_archive(self, mock_call):
""" Edit archive file. """ """ Edit archive file. """
mock_call.return_value = 0 mock_call.return_value = 0
...@@ -148,11 +148,29 @@ class EditCommandTest(CommandTest): ...@@ -148,11 +148,29 @@ class EditCommandTest(CommandTest):
os.environ['EDITOR'] = editor os.environ['EDITOR'] = editor
archive = config().archive() archive = config().archive()
command = EditCommand([u("-d")], self.todolist, self.out, self.error, None) command = EditCommand(["-d"], self.todolist, self.out, self.error, None)
command.execute() command.execute()
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
mock_call.assert_called_once_with([editor, archive]) mock_call.assert_called_once_with([editor, archive])
@mock.patch('topydo.commands.EditCommand.check_call')
def test_edit_todotxt(self, mock_call):
""" Edit todo file. """
mock_call.return_value = 0
editor = 'vi'
os.environ['EDITOR'] = editor
todotxt = config().todotxt()
result = self.todolist.print_todos() # copy TodoList content *before* executing command
command = EditCommand([], self.todolist, self.out, self.error, None)
command.execute()
self.assertEqual(self.errors, "")
self.assertEqual(self.todolist.print_todos(), result)
mock_call.assert_called_once_with([editor, todotxt])
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -307,7 +307,7 @@ class OrdinalTagFilterTest(TopydoTest): ...@@ -307,7 +307,7 @@ class OrdinalTagFilterTest(TopydoTest):
self.todo1 = "Foo due:{}".format(self.today) self.todo1 = "Foo due:{}".format(self.today)
self.todo2 = "Bar due:{}".format(self.tomorrow) self.todo2 = "Bar due:{}".format(self.tomorrow)
self.todo3 = "Baz due:nonsense" self.todo3 = "Baz due:Nonsense"
self.todo4 = "Fnord due:2014-10-32" self.todo4 = "Fnord due:2014-10-32"
self.todos = [ self.todos = [
...@@ -358,6 +358,22 @@ class OrdinalTagFilterTest(TopydoTest): ...@@ -358,6 +358,22 @@ class OrdinalTagFilterTest(TopydoTest):
self.assertEqual(len(result), 1) self.assertEqual(len(result), 1)
self.assertEqual(result[0].source(), self.todo2) self.assertEqual(result[0].source(), self.todo2)
def test_filter6(self):
otf = Filter.OrdinalTagFilter('due:non')
result = otf.filter(self.todos)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].source(), self.todo3)
def test_filter7(self):
otf = Filter.OrdinalTagFilter('due:Non')
result = otf.filter(self.todos)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].source(), self.todo3)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
# 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/>.
from topydo.lib.JsonPrinter import JsonPrinter
from topydo.lib.Todo import Todo
from test.TopydoTest import TopydoTest
class JsonPrinterTest(TopydoTest):
"""
Tests the functionality of printing a single todo item. Printing a list is
already covered by the ListCommand tests.
"""
def test_json(self):
""" Print a single todo item. """
printer = JsonPrinter()
todo = Todo('2015-06-06 Foo due:2015-05-32')
result = printer.print_todo(todo)
self.assertEqual(result, '{"completed": false, "completion_date": null, "contexts": [], "creation_date": "2015-06-06", "priority": null, "projects": [], "source": "2015-06-06 Foo due:2015-05-32", "tags": [["due", "2015-05-32"]], "text": "Foo"}')
...@@ -224,7 +224,6 @@ class ListCommandUnicodeTest(CommandTest): ...@@ -224,7 +224,6 @@ class ListCommandUnicodeTest(CommandTest):
self.assertEqual(self.output, expected) self.assertEqual(self.output, expected)
class ListCommandJsonTest(CommandTest): class ListCommandJsonTest(CommandTest):
def test_json(self): def test_json(self):
todolist = load_file_to_todolist("test/data/ListCommandTest.txt") todolist = load_file_to_todolist("test/data/ListCommandTest.txt")
...@@ -265,11 +264,14 @@ def replace_ical_tags(p_text): ...@@ -265,11 +264,14 @@ def replace_ical_tags(p_text):
IS_PYTHON_32 = (sys.version_info.major, sys.version_info.minor) == (3, 2) IS_PYTHON_32 = (sys.version_info.major, sys.version_info.minor) == (3, 2)
class ListCommandIcalTest(CommandTest): class ListCommandIcalTest(CommandTest):
def setUp(self):
self.maxDiff = None
@unittest.skipIf(IS_PYTHON_32, "icalendar is not supported for Python 3.2") @unittest.skipIf(IS_PYTHON_32, "icalendar is not supported for Python 3.2")
def test_ical(self): def test_ical(self):
todolist = load_file_to_todolist("test/data/ListCommandTest.txt") todolist = load_file_to_todolist("test/data/ListCommandIcalTest.txt")
command = ListCommand(["-f", "ical"], todolist, self.out, self.error) command = ListCommand(["-x", "-f", "ical"], todolist, self.out, self.error)
command.execute() command.execute()
self.assertTrue(todolist.is_dirty()) self.assertTrue(todolist.is_dirty())
......
...@@ -37,6 +37,7 @@ class PostponeCommandTest(CommandTest): ...@@ -37,6 +37,7 @@ class PostponeCommandTest(CommandTest):
"Baz due:{} t:{}".format(self.today.isoformat(), self.start.isoformat()), "Baz due:{} t:{}".format(self.today.isoformat(), self.start.isoformat()),
"Past due:{}".format(self.past.isoformat()), "Past due:{}".format(self.past.isoformat()),
"Future due:{} t:{}".format(self.future.isoformat(), self.future_start.isoformat()), "Future due:{} t:{}".format(self.future.isoformat(), self.future_start.isoformat()),
"FutureStart t:{}".format(self.future.isoformat())
] ]
self.todolist = TodoList(todos) self.todolist = TodoList(todos)
...@@ -233,6 +234,55 @@ class PostponeCommandTest(CommandTest): ...@@ -233,6 +234,55 @@ class PostponeCommandTest(CommandTest):
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, u("Invalid todo number given: Fo\u00d3B\u0105r.\n")) self.assertEqual(self.errors, u("Invalid todo number given: Fo\u00d3B\u0105r.\n"))
def test_expr_postpone1(self):
command = PostponeCommand(["-e", "due:tod", "2w"], self.todolist, self.out, self.error, None)
command.execute()
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())
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_expr_postpone2(self):
cmd_args = ["-e", "t:{}".format(self.start.isoformat()), "due:tod", "1w"]
command = PostponeCommand(cmd_args, self.todolist, self.out, self.error, None)
command.execute()
due = self.today + timedelta(7)
result = "| 3| Baz due:{} t:{}\n".format(due.isoformat(), self.start.isoformat())
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_expr_postpone3(self):
command = PostponeCommand(["-e", "@test", "due:tod", "+project", "C"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_postpone4(self):
""" Don't postpone unrelevant todo items. """
command = PostponeCommand(["-e", "FutureStart", "1w"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_postpone5(self):
""" Force postponing unrelevant items with additional -x flag. """
command = PostponeCommand(["-xe", "FutureStart", "1w"], self.todolist, self.out, self.error, None)
command.execute()
due = self.today + timedelta(7)
result = "| 6| FutureStart t:{} due:{}\n".format(self.future.isoformat(), due.isoformat())
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_help(self): def test_help(self):
command = PostponeCommand(["help"], self.todolist, self.out, self.error) command = PostponeCommand(["help"], self.todolist, self.out, self.error)
command.execute() command.execute()
......
...@@ -27,6 +27,9 @@ class PriorityCommandTest(CommandTest): ...@@ -27,6 +27,9 @@ class PriorityCommandTest(CommandTest):
todos = [ todos = [
"(A) Foo", "(A) Foo",
"Bar", "Bar",
"(B) a @test with due:2015-06-03",
"a @test with +project p:1",
"Baz id:1",
] ]
self.todolist = TodoList(todos) self.todolist = TodoList(todos)
...@@ -71,6 +74,58 @@ class PriorityCommandTest(CommandTest): ...@@ -71,6 +74,58 @@ class PriorityCommandTest(CommandTest):
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, "")
def test_set_prio6(self):
""" Allow priority to be set including parentheses. """
command = PriorityCommand(["Foo", "2", "(C)"], self.todolist, self.out, self.error)
command.execute()
self.assertTrue(self.todolist.is_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_expr_prio1(self):
command = PriorityCommand(["-e", "@test", "C"], self.todolist, self.out, self.error, None)
command.execute()
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.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_expr_prio2(self):
command = PriorityCommand(["-e", "@test", "due:2015-06-03", "C"], self.todolist, self.out, self.error, None)
command.execute()
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.assertEqual(self.output, result)
self.assertEqual(self.errors, "")
def test_expr_prio3(self):
command = PriorityCommand(["-e", "@test", "due:2015-06-03", "+project", "C"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_prio4(self):
""" Don't prioritize unrelevant todo items. """
command = PriorityCommand(["-e", "Baz", "C"], self.todolist, self.out, self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
def test_expr_prio5(self):
""" Force prioritizing unrelevant items with additional -x flag. """
command = PriorityCommand(["-xe", "Baz", "D"], self.todolist, self.out, self.error, None)
command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, "Priority set to D.\n| 5| (D) Baz id:1\n")
self.assertEqual(self.errors, "")
def test_invalid1(self): def test_invalid1(self):
command = PriorityCommand(["99", "A"], self.todolist, self.out, self.error) command = PriorityCommand(["99", "A"], self.todolist, self.out, self.error)
command.execute() command.execute()
...@@ -128,6 +183,29 @@ class PriorityCommandTest(CommandTest): ...@@ -128,6 +183,29 @@ class PriorityCommandTest(CommandTest):
self.assertEqual(self.output, "") self.assertEqual(self.output, "")
self.assertEqual(self.errors, u("Invalid todo number given: Fo\u00d3B\u0105r.\n")) self.assertEqual(self.errors, u("Invalid todo number given: Fo\u00d3B\u0105r.\n"))
def test_invalid8(self):
"""
Test that there's only one capital surrounded by non-word
characters that makes up a priority.
"""
command = PriorityCommand(["2", "(Aa)"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid priority given.\n")
def test_invalid9(self):
"""
Test that there's only one capital surrounded by non-word
characters that makes up a priority.
"""
command = PriorityCommand(["2", "Aa"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.output, "")
self.assertEqual(self.errors, "Invalid priority given.\n")
def test_empty(self): def test_empty(self):
command = PriorityCommand([], self.todolist, self.out, self.error) command = PriorityCommand([], self.todolist, self.out, self.error)
command.execute() command.execute()
......
...@@ -18,7 +18,7 @@ from datetime import date, timedelta ...@@ -18,7 +18,7 @@ from datetime import date, timedelta
import unittest import unittest
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib.Recurrence import advance_recurring_todo, strict_advance_recurring_todo, NoRecurrenceException from topydo.lib.Recurrence import advance_recurring_todo, NoRecurrenceException
from topydo.lib.Todo import Todo from topydo.lib.Todo import Todo
from test.TopydoTest import TopydoTest from test.TopydoTest import TopydoTest
...@@ -26,6 +26,7 @@ class RecurrenceTest(TopydoTest): ...@@ -26,6 +26,7 @@ class RecurrenceTest(TopydoTest):
def setUp(self): def setUp(self):
super(RecurrenceTest, self).setUp() super(RecurrenceTest, self).setUp()
self.todo = Todo("Test rec:1w") self.todo = Todo("Test rec:1w")
self.stricttodo = Todo("Test rec:+1w")
def test_duedate1(self): def test_duedate1(self):
""" Where due date is in the future. """ """ Where due date is in the future. """
...@@ -63,7 +64,7 @@ class RecurrenceTest(TopydoTest): ...@@ -63,7 +64,7 @@ class RecurrenceTest(TopydoTest):
new_due = date.today() - timedelta(1) new_due = date.today() - timedelta(1)
self.todo.set_tag(config().tag_due(), past.isoformat()) self.todo.set_tag(config().tag_due(), past.isoformat())
new_todo = strict_advance_recurring_todo(self.todo) new_todo = advance_recurring_todo(self.todo, p_strict=True)
self.assertEqual(new_todo.due_date(), new_due) self.assertEqual(new_todo.due_date(), new_due)
...@@ -73,7 +74,7 @@ class RecurrenceTest(TopydoTest): ...@@ -73,7 +74,7 @@ class RecurrenceTest(TopydoTest):
new_due = date.today() + timedelta(8) new_due = date.today() + timedelta(8)
self.todo.set_tag(config().tag_due(), future.isoformat()) self.todo.set_tag(config().tag_due(), future.isoformat())
new_todo = strict_advance_recurring_todo(self.todo) new_todo = advance_recurring_todo(self.todo, p_strict=True)
self.assertEqual(new_todo.due_date(), new_due) self.assertEqual(new_todo.due_date(), new_due)
...@@ -83,7 +84,7 @@ class RecurrenceTest(TopydoTest): ...@@ -83,7 +84,7 @@ class RecurrenceTest(TopydoTest):
new_due = date.today() + timedelta(7) new_due = date.today() + timedelta(7)
self.todo.set_tag(config().tag_due(), today.isoformat()) self.todo.set_tag(config().tag_due(), today.isoformat())
new_todo = strict_advance_recurring_todo(self.todo) new_todo = advance_recurring_todo(self.todo, p_strict=True)
self.assertEqual(new_todo.due_date(), new_due) self.assertEqual(new_todo.due_date(), new_due)
...@@ -96,7 +97,7 @@ class RecurrenceTest(TopydoTest): ...@@ -96,7 +97,7 @@ class RecurrenceTest(TopydoTest):
def test_noduedate2(self): def test_noduedate2(self):
new_due = date.today() + timedelta(7) new_due = date.today() + timedelta(7)
new_todo = strict_advance_recurring_todo(self.todo) new_todo = advance_recurring_todo(self.todo, p_strict=True)
self.assertTrue(new_todo.has_tag(config().tag_due())) self.assertTrue(new_todo.has_tag(config().tag_due()))
self.assertEqual(new_todo.due_date(), new_due) self.assertEqual(new_todo.due_date(), new_due)
...@@ -121,7 +122,7 @@ class RecurrenceTest(TopydoTest): ...@@ -121,7 +122,7 @@ class RecurrenceTest(TopydoTest):
self.todo.set_tag(config().tag_start(), yesterday.isoformat()) self.todo.set_tag(config().tag_start(), yesterday.isoformat())
new_start = date.today() + timedelta(5) new_start = date.today() + timedelta(5)
new_todo = strict_advance_recurring_todo(self.todo) new_todo = advance_recurring_todo(self.todo, p_strict=True)
self.assertEqual(new_todo.start_date(), new_start) self.assertEqual(new_todo.start_date(), new_start)
...@@ -135,6 +136,32 @@ class RecurrenceTest(TopydoTest): ...@@ -135,6 +136,32 @@ class RecurrenceTest(TopydoTest):
self.assertEqual(new_todo.start_date(), new_start) self.assertEqual(new_todo.start_date(), new_start)
def test_strict_recurrence1(self):
"""
Strict recurrence where due date is in the past, using + notation in
expression.
"""
past = date.today() - timedelta(8)
new_due = date.today() - timedelta(1)
self.stricttodo.set_tag(config().tag_due(), past.isoformat())
new_todo = advance_recurring_todo(self.stricttodo, p_strict=True)
self.assertEqual(new_todo.due_date(), new_due)
def test_strict_recurrence2(self):
"""
Strict recurrence where due date is in the future, using + notation in
expression.
"""
future = date.today() + timedelta(1)
new_due = date.today() + timedelta(8)
self.stricttodo.set_tag(config().tag_due(), future.isoformat())
new_todo = advance_recurring_todo(self.stricttodo, p_strict=True)
self.assertEqual(new_todo.due_date(), new_due)
def test_no_recurrence(self): def test_no_recurrence(self):
self.todo.remove_tag('rec') self.todo.remove_tag('rec')
self.assertRaises(NoRecurrenceException, advance_recurring_todo, self.todo) self.assertRaises(NoRecurrenceException, advance_recurring_todo, self.todo)
......
...@@ -24,6 +24,7 @@ from topydo.lib.Todo import Todo ...@@ -24,6 +24,7 @@ from topydo.lib.Todo import Todo
from topydo.lib.TodoFile import TodoFile from topydo.lib.TodoFile import TodoFile
from topydo.lib.TodoListBase import InvalidTodoException from topydo.lib.TodoListBase import InvalidTodoException
from topydo.lib.TodoList import TodoList from topydo.lib.TodoList import TodoList
from topydo.lib.TodoListBase import TodoListBase
from test.TopydoTest import TopydoTest from test.TopydoTest import TopydoTest
class TodoListTester(TopydoTest): class TodoListTester(TopydoTest):
...@@ -34,7 +35,7 @@ class TodoListTester(TopydoTest): ...@@ -34,7 +35,7 @@ class TodoListTester(TopydoTest):
lines = [line for line in self.todofile.read() \ lines = [line for line in self.todofile.read() \
if re.search(r'\S', line)] if re.search(r'\S', line)]
self.text = ''.join(lines) self.text = ''.join(lines)
self.todolist = TodoList(lines) self.todolist = TodoListBase(lines)
def test_contexts(self): def test_contexts(self):
self.assertEqual(set(['Context1', 'Context2']), \ self.assertEqual(set(['Context1', 'Context2']), \
...@@ -101,6 +102,16 @@ class TodoListTester(TopydoTest): ...@@ -101,6 +102,16 @@ class TodoListTester(TopydoTest):
self.assertTrue(self.todolist.is_dirty()) self.assertTrue(self.todolist.is_dirty())
self.assertRaises(InvalidTodoException, self.todolist.number, todo) self.assertRaises(InvalidTodoException, self.todolist.number, todo)
def test_delete2(self):
""" Try to remove a todo item that does not exist. """
count = self.todolist.count()
todo = Todo('Not in the list')
self.todolist.delete(todo)
self.assertEqual(self.todolist.count(), count)
self.assertFalse(self.todolist.is_dirty())
def test_append1(self): def test_append1(self):
todo = self.todolist.todo(3) todo = self.todolist.todo(3)
self.todolist.append(todo, "@Context3") self.todolist.append(todo, "@Context3")
...@@ -137,13 +148,6 @@ class TodoListTester(TopydoTest): ...@@ -137,13 +148,6 @@ class TodoListTester(TopydoTest):
""" Test that empty lines are not counted. """ """ Test that empty lines are not counted. """
self.assertEqual(self.todolist.count(), 5) self.assertEqual(self.todolist.count(), 5)
def test_todo_by_dep_id(self):
""" Tests that todos can be retrieved by their id tag. """
self.todolist.add("(C) Foo id:1")
self.assertTrue(self.todolist.todo_by_dep_id('1'))
self.assertFalse(self.todolist.todo_by_dep_id('2'))
def test_todo_number1(self): def test_todo_number1(self):
todo = Todo("No number") todo = Todo("No number")
self.todolist.add_todo(todo) self.todolist.add_todo(todo)
...@@ -349,6 +353,14 @@ class TodoListDependencyTester(TopydoTest): ...@@ -349,6 +353,14 @@ class TodoListDependencyTester(TopydoTest):
self.assertEqual(todo1.source(), 'Foo id:1') self.assertEqual(todo1.source(), 'Foo id:1')
self.assertEqual(todo2.source(), 'Bar p:1') self.assertEqual(todo2.source(), 'Bar p:1')
def test_todo_by_dep_id(self):
""" Tests that todos can be retrieved by their id tag. """
todolist = TodoList([])
todolist.add("(C) Foo id:1")
self.assertTrue(todolist.todo_by_dep_id('1'))
self.assertFalse(todolist.todo_by_dep_id('2'))
class TodoListCleanDependencyTester(TopydoTest): class TodoListCleanDependencyTester(TopydoTest):
def setUp(self): def setUp(self):
super(TodoListCleanDependencyTester, self).setUp() super(TodoListCleanDependencyTester, self).setUp()
......
(C) Foo @Context2 Not@Context +Project1 Not+Project
(D) Bar @Context1 +Project2 p:1 due:2015-06-06
(C) Baz @Context1 +Project1 key:value id:1
(C) 2015-06-06 Drink beer @ home
(G) 13 + 29 = 42
(C) Only a start date t:2015-06-06
x 2015-06-06 A completed item due:2015-05-05
This diff was suppressed by a .gitattributes entry.
...@@ -7,6 +7,9 @@ default_command = ls ...@@ -7,6 +7,9 @@ default_command = ls
colors = 1 colors = 1
identifiers = linenumber ; or: text identifiers = linenumber ; or: text
[add]
auto_creation_date = 1
[ls] [ls]
hide_tags = id,p,ical hide_tags = id,p,ical
indent = 0 indent = 0
......
...@@ -36,6 +36,8 @@ _SUBCOMMAND_MAP = { ...@@ -36,6 +36,8 @@ _SUBCOMMAND_MAP = {
'ls': 'ListCommand', 'ls': 'ListCommand',
'lscon': 'ListContextCommand', 'lscon': 'ListContextCommand',
'listcon': 'ListContextCommand', 'listcon': 'ListContextCommand',
'listcontext': 'ListContextCommand',
'listcontexts': 'ListContextCommand',
'lsprj': 'ListProjectCommand', 'lsprj': 'ListProjectCommand',
'lsproj': 'ListProjectCommand', 'lsproj': 'ListProjectCommand',
'listprj': 'ListProjectCommand', 'listprj': 'ListProjectCommand',
......
...@@ -24,16 +24,17 @@ import sys ...@@ -24,16 +24,17 @@ import sys
from six import PY2 from six import PY2
from six.moves import input from six.moves import input
MAIN_OPTS = "c:d:ht:v" MAIN_OPTS = "ac:d:ht:v"
def usage(): def usage():
""" Prints the command-line usage of topydo. """ """ Prints the command-line usage of topydo. """
print("""\ print("""\
Synopsis: topydo [-c <config>] [-d <archive>] [-t <todo.txt>] subcommand [help|args] Synopsis: topydo [-a] [-c <config>] [-d <archive>] [-t <todo.txt>] subcommand [help|args]
topydo -h topydo -h
topydo -v topydo -v
-a : Do not archive todo items on completion.
-c : Specify an alternative configuration file. -c : Specify an alternative configuration file.
-d : Specify an alternative archive file (done.txt) -d : Specify an alternative archive file (done.txt)
-h : This help text -h : This help text
...@@ -112,6 +113,7 @@ class CLIApplicationBase(object): ...@@ -112,6 +113,7 @@ class CLIApplicationBase(object):
def __init__(self): def __init__(self):
self.todolist = TodoList.TodoList([]) self.todolist = TodoList.TodoList([])
self.todofile = None self.todofile = None
self.do_archive = True
def _usage(self): def _usage(self):
usage() usage()
...@@ -133,7 +135,9 @@ class CLIApplicationBase(object): ...@@ -133,7 +135,9 @@ class CLIApplicationBase(object):
overrides = {} overrides = {}
for opt, value in opts: for opt, value in opts:
if opt == "-c": if opt == "-a":
self.do_archive = False
elif opt == "-c":
alt_config_path = value alt_config_path = value
elif opt == "-t": elif opt == "-t":
overrides[('topydo', 'filename')] = value overrides[('topydo', 'filename')] = value
...@@ -203,8 +207,12 @@ class CLIApplicationBase(object): ...@@ -203,8 +207,12 @@ class CLIApplicationBase(object):
completed. It will do some maintenance and write out the final result completed. It will do some maintenance and write out the final result
to the todo.txt file. to the todo.txt file.
""" """
# do not archive when the value of the filename is an empty string
# (i.e. explicitly left empty in the configuration
if self.todolist.is_dirty(): if self.todolist.is_dirty():
self._archive() if self.do_archive and config().archive():
self._archive()
if config().keep_sorted(): if config().keep_sorted():
self._execute(SortCommand, []) self._execute(SortCommand, [])
......
...@@ -22,7 +22,7 @@ import sys ...@@ -22,7 +22,7 @@ import sys
from topydo.cli.CLIApplicationBase import CLIApplicationBase, error, usage from topydo.cli.CLIApplicationBase import CLIApplicationBase, error, usage
from topydo.cli.TopydoCompleter import TopydoCompleter from topydo.cli.TopydoCompleter import TopydoCompleter
from prompt_toolkit.shortcuts import get_input from prompt_toolkit.shortcuts import get_input
from prompt_toolkit.history import History from prompt_toolkit.history import InMemoryHistory
from topydo.lib.Config import config, ConfigError from topydo.lib.Config import config, ConfigError
...@@ -83,7 +83,7 @@ class PromptApplication(CLIApplicationBase): ...@@ -83,7 +83,7 @@ class PromptApplication(CLIApplicationBase):
def run(self): def run(self):
""" Main entry function. """ """ Main entry function. """
history = History() history = InMemoryHistory()
while True: while True:
# (re)load the todo.txt file (only if it has been modified) # (re)load the todo.txt file (only if it has been modified)
...@@ -91,7 +91,8 @@ class PromptApplication(CLIApplicationBase): ...@@ -91,7 +91,8 @@ class PromptApplication(CLIApplicationBase):
try: try:
user_input = get_input(u'topydo> ', history=history, user_input = get_input(u'topydo> ', history=history,
completer=self.completer).split() completer=self.completer,
complete_while_typing=False).split()
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
sys.exit(0) sys.exit(0)
......
...@@ -14,6 +14,11 @@ ...@@ -14,6 +14,11 @@
# 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/>.
"""
This module provides a completer class that can be used by get_input provided
by the prompt toolkit.
"""
import datetime import datetime
import re import re
...@@ -23,84 +28,95 @@ from topydo.lib.Config import config ...@@ -23,84 +28,95 @@ from topydo.lib.Config import config
from topydo.Commands import _SUBCOMMAND_MAP from topydo.Commands import _SUBCOMMAND_MAP
from topydo.lib.RelativeDate import relative_date_to_date from topydo.lib.RelativeDate import relative_date_to_date
def _date_suggestions(): def _subcommands(p_word_before_cursor):
""" """ Generator for subcommand name completion. """
Returns a list of relative date that is presented to the user as auto subcommands = [sc for sc in sorted(_SUBCOMMAND_MAP.keys()) if
complete suggestions. sc.startswith(p_word_before_cursor)]
""" for command in subcommands:
# don't use strftime, prevent locales to kick in yield Completion(command, -len(p_word_before_cursor))
days_of_week = {
0: "Monday", def _dates(p_word_before_cursor):
1: "Tuesday", """ Generator for date completion. """
2: "Wednesday", def _date_suggestions():
3: "Thursday", """
4: "Friday", Returns a list of relative date that is presented to the user as auto
5: "Saturday", complete suggestions.
6: "Sunday" """
} # don't use strftime, prevent locales to kick in
days_of_week = {
dates = [ 0: "Monday",
'today', 1: "Tuesday",
'tomorrow', 2: "Wednesday",
] 3: "Thursday",
4: "Friday",
# show days of week up to next week 5: "Saturday",
dow = datetime.date.today().weekday() 6: "Sunday"
for i in range(dow + 2 % 7, dow + 7): }
dates.append(days_of_week[i % 7])
dates = [
# and some more relative days starting from next week 'today',
dates += ["1w", "2w", "1m", "2m", "3m", "1y"] 'tomorrow',
]
return dates
# show days of week up to next week
dow = datetime.date.today().weekday()
for i in range(dow + 2 % 7, dow + 7):
dates.append(days_of_week[i % 7])
# and some more relative days starting from next week
dates += ["1w", "2w", "1m", "2m", "3m", "1y"]
return dates
to_absolute = lambda s: relative_date_to_date(s).isoformat()
start_value_pos = p_word_before_cursor.find(':') + 1
value = p_word_before_cursor[start_value_pos:]
for reldate in _date_suggestions():
if not reldate.startswith(value):
continue
yield Completion(reldate, -len(value), display_meta=to_absolute(reldate))
class TopydoCompleter(Completer): class TopydoCompleter(Completer):
"""
Completer class that completes projects, contexts, dates and
subcommands.
"""
def __init__(self, p_todolist): def __init__(self, p_todolist):
self.todolist = p_todolist self.todolist = p_todolist
def _subcommands(self, p_word_before_cursor):
subcommands = [sc for sc in sorted(_SUBCOMMAND_MAP.keys()) if sc.startswith(p_word_before_cursor)]
for command in subcommands:
yield Completion(command, -len(p_word_before_cursor))
def _projects(self, p_word_before_cursor): def _projects(self, p_word_before_cursor):
projects = [p for p in self.todolist.projects() if p.startswith(p_word_before_cursor[1:])] """ Generator for project completion. """
projects = [p for p in self.todolist.projects() if
p.startswith(p_word_before_cursor[1:])]
for project in projects: for project in projects:
yield Completion("+" + project, -len(p_word_before_cursor)) yield Completion("+" + project, -len(p_word_before_cursor))
def _contexts(self, p_word_before_cursor): def _contexts(self, p_word_before_cursor):
contexts = [c for c in self.todolist.contexts() if c.startswith(p_word_before_cursor[1:])] """ Generator for context completion. """
contexts = [c for c in self.todolist.contexts() if
c.startswith(p_word_before_cursor[1:])]
for context in contexts: for context in contexts:
yield Completion("@" + context, -len(p_word_before_cursor)) yield Completion("@" + context, -len(p_word_before_cursor))
def _dates(self, p_word_before_cursor):
to_absolute = lambda s: relative_date_to_date(s).isoformat()
start_value_pos = p_word_before_cursor.find(':') + 1
value = p_word_before_cursor[start_value_pos:]
for reldate in _date_suggestions():
if not reldate.startswith(value):
continue
yield Completion(reldate, -len(value), display_meta=to_absolute(reldate))
def get_completions(self, p_document, _): def get_completions(self, p_document, _):
# include all characters except whitespaces (for + and @) # include all characters except whitespaces (for + and @)
word_before_cursor = p_document.get_word_before_cursor(True) word_before_cursor = p_document.get_word_before_cursor(True)
is_first_word = not re.match(r'\s*\S+\s', p_document.current_line_before_cursor) is_first_word = not re.match(r'\s*\S+\s', p_document.current_line_before_cursor)
if is_first_word: if is_first_word:
return self._subcommands(word_before_cursor) return _subcommands(word_before_cursor)
elif word_before_cursor.startswith('+'): elif word_before_cursor.startswith('+'):
return self._projects(word_before_cursor) return self._projects(word_before_cursor)
elif word_before_cursor.startswith('@'): elif word_before_cursor.startswith('@'):
return self._contexts(word_before_cursor) return self._contexts(word_before_cursor)
elif word_before_cursor.startswith(config().tag_due() + ':'): elif word_before_cursor.startswith(config().tag_due() + ':'):
return self._dates(word_before_cursor) return _dates(word_before_cursor)
elif word_before_cursor.startswith(config().tag_start() + ':'): elif word_before_cursor.startswith(config().tag_start() + ':'):
return self._dates(word_before_cursor) return _dates(word_before_cursor)
return [] return []
...@@ -20,6 +20,10 @@ import sys ...@@ -20,6 +20,10 @@ import sys
import getopt import getopt
from topydo.cli.CLIApplicationBase import MAIN_OPTS, error from topydo.cli.CLIApplicationBase import MAIN_OPTS, error
from topydo.cli.CLI import CLIApplication from topydo.cli.CLI import CLIApplication
# enable color on windows CMD
if "win32" in sys.platform:
import colorama
colorama.init()
def main(): def main():
""" Main entry point of the CLI. """ """ Main entry point of the CLI. """
......
...@@ -47,7 +47,6 @@ class AddCommand(Command): ...@@ -47,7 +47,6 @@ class AddCommand(Command):
self.args = args self.args = args
def get_todos_from_file(self): def get_todos_from_file(self):
if self.from_file == '-': if self.from_file == '-':
f = stdin f = stdin
...@@ -107,7 +106,8 @@ class AddCommand(Command): ...@@ -107,7 +106,8 @@ class AddCommand(Command):
add_dependencies('before') add_dependencies('before')
add_dependencies('after') add_dependencies('after')
p_todo.set_creation_date(date.today()) 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)
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +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 topydo.commands.DCommand import DCommand from topydo.lib.DCommand import DCommand
class DeleteCommand(DCommand): class DeleteCommand(DCommand):
def __init__(self, p_args, p_todolist, def __init__(self, p_args, p_todolist,
...@@ -38,8 +38,16 @@ class DeleteCommand(DCommand): ...@@ -38,8 +38,16 @@ class DeleteCommand(DCommand):
self.execute_specific_core(p_todo) self.execute_specific_core(p_todo)
def usage(self): def usage(self):
return """Synopsis: del [-f] <NUMBER1> [<NUMBER2> ...]""" return """\
Synopsis: del [-f] <NUMBER1> [<NUMBER2> ...]
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
the -e flag. Use -x to also process todo items that are normally invisible
(with the 'ls' subcommand).
"""
...@@ -25,23 +25,25 @@ class DepriCommand(MultiCommand): ...@@ -25,23 +25,25 @@ class DepriCommand(MultiCommand):
super(DepriCommand, self).__init__( super(DepriCommand, self).__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.get_todos(self.args) def _execute_multi_specific(self):
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
def execute_multi_specific(self): for todo in self.todos:
try: if todo.priority() != None:
self.printer.add_filter(PrettyPrinterNumbers(self.todolist)) self.todolist.set_priority(todo, None)
self.out("Priority removed.")
for todo in self.todos: self.out(self.printer.print_todo(todo))
if todo.priority() != None:
self.todolist.set_priority(todo, None)
self.out("Priority removed.")
self.out(self.printer.print_todo(todo))
except IndexError:
self.error(self.usage())
def usage(self): def usage(self):
return """Synopsis: depri <NUMBER1> [<NUMBER2> ...]""" return """\
Synopsis: depri <NUMBER1> [<NUMBER2> ...]
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
the -e flag. Use -x to also process todo items that are normally invisible
(with the 'ls' subcommand).
"""
...@@ -16,10 +16,10 @@ ...@@ -16,10 +16,10 @@
from datetime import date from datetime import date
from topydo.commands.DCommand import DCommand from topydo.lib.DCommand import DCommand
from topydo.lib.PrettyPrinter import PrettyPrinter from topydo.lib.PrettyPrinter import PrettyPrinter
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.Recurrence import advance_recurring_todo, strict_advance_recurring_todo, NoRecurrenceException from topydo.lib.Recurrence import advance_recurring_todo, NoRecurrenceException
from topydo.lib.Utils import date_string_to_date from topydo.lib.Utils import date_string_to_date
class DoCommand(DCommand): class DoCommand(DCommand):
...@@ -36,9 +36,13 @@ class DoCommand(DCommand): ...@@ -36,9 +36,13 @@ class DoCommand(DCommand):
def get_flags(self): def get_flags(self):
""" Additional flags. """ """ Additional flags. """
return ("d:s", ["date=", "strict"]) opts, long_opts = super(DoCommand, self).get_flags()
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)
if p_opt == "-s" or p_opt == "--strict": if p_opt == "-s" or p_opt == "--strict":
self.strict_recurrence = True self.strict_recurrence = True
elif p_opt == "-d" or p_opt == "--date": elif p_opt == "-d" or p_opt == "--date":
...@@ -50,12 +54,11 @@ class DoCommand(DCommand): ...@@ -50,12 +54,11 @@ class DoCommand(DCommand):
def _handle_recurrence(self, p_todo): def _handle_recurrence(self, p_todo):
if p_todo.has_tag('rec'): if p_todo.has_tag('rec'):
try: try:
if self.strict_recurrence: new_todo = advance_recurring_todo(
new_todo = strict_advance_recurring_todo(p_todo, p_todo,
self.completion_date) p_offset=self.completion_date,
else: p_strict=self.strict_recurrence
new_todo = advance_recurring_todo(p_todo, )
self.completion_date)
self.todolist.add_todo(new_todo) self.todolist.add_todo(new_todo)
...@@ -96,11 +99,18 @@ class DoCommand(DCommand): ...@@ -96,11 +99,18 @@ class DoCommand(DCommand):
self.todolist.set_todo_completed(p_todo, self.completion_date) self.todolist.set_todo_completed(p_todo, self.completion_date)
def usage(self): def usage(self):
return """Synopsis: do [--date] [--force] [--strict] <NUMBER1> [<NUMBER2> ...]""" return """\
Synopsis: do [--date] [--force] [--strict] <NUMBER1> [<NUMBER2> ...]
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
-e flag. Use -x to also process todo items that are normally invisible (with
the 'ls' subcommand).
In case a todo has subitems, a question is asked whether the subitems should be In case a todo has subitems, a question is asked whether the subitems should be
marked as completed as well. When --force is given, no interaction is required marked as completed as well. When --force is given, no interaction is required
and the subitems are not marked completed. and the subitems are not marked completed.
......
...@@ -34,26 +34,25 @@ DEFAULT_EDITOR = 'vi' ...@@ -34,26 +34,25 @@ DEFAULT_EDITOR = 'vi'
# cannot use super() inside the class itself # cannot use super() inside the class itself
BASE_TODOLIST = lambda tl: super(TodoList, tl) BASE_TODOLIST = lambda tl: super(TodoList, tl)
class EditCommand(MultiCommand, ExpressionCommand): 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(EditCommand, self).__init__(p_args, p_todolist, p_output,
p_error, p_input) p_error, p_input)
if len(self.args) == 0:
self.multi_mode = False
self.is_expression = False self.is_expression = False
self.edit_archive = False self.edit_archive = False
self.last_argument = False
def _process_flags(self): def get_flags(self):
opts, args = self.getopt('xed') return ("d", [])
for opt, value in opts:
if opt == '-d':
self.edit_archive = True
elif opt == '-x':
self.show_all = True
elif opt == '-e':
self.is_expression = True
self.args = args def process_flag(self, p_opt, p_value):
if p_opt == '-d':
self.edit_archive = True
self.multi_mode = False
def _todos_to_temp(self): def _todos_to_temp(self):
f = tempfile.NamedTemporaryFile() f = tempfile.NamedTemporaryFile()
...@@ -73,12 +72,20 @@ class EditCommand(MultiCommand, ExpressionCommand): ...@@ -73,12 +72,20 @@ class EditCommand(MultiCommand, ExpressionCommand):
return todo_objs return todo_objs
def _open_in_editor(self, p_temp_file, p_editor): def _open_in_editor(self, p_file):
try:
editor = os.environ['EDITOR'] or DEFAULT_EDITOR
except(KeyError):
editor = DEFAULT_EDITOR
try: try:
return check_call([p_editor, p_temp_file.name]) return check_call([editor, p_file])
except CalledProcessError: except CalledProcessError:
self.error('Something went wrong in the editor...') self.error('Something went wrong in the editor...')
return 1 return 1
except(OSError):
self.error('There is no such editor as: ' + editor + '. '
'Check your $EDITOR and/or $PATH')
def _catch_todo_errors(self): def _catch_todo_errors(self):
errors = [] errors = []
...@@ -94,59 +101,35 @@ class EditCommand(MultiCommand, ExpressionCommand): ...@@ -94,59 +101,35 @@ class EditCommand(MultiCommand, ExpressionCommand):
else: else:
return None return None
def execute(self): def _execute_multi_specific(self):
if not super(EditCommand, self).execute():
return False
self.printer.add_filter(PrettyPrinterNumbers(self.todolist)) self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
try:
editor = os.environ['EDITOR'] or DEFAULT_EDITOR
except(KeyError):
editor = DEFAULT_EDITOR
try: temp_todos = self._todos_to_temp()
if len(self.args) < 1:
todo = config().todotxt()
return call([editor, todo]) == 0 if not self._open_in_editor(temp_todos.name):
new_todos = self._todos_from_temp(temp_todos)
if len(new_todos) == len(self.todos):
for todo in self.todos:
BASE_TODOLIST(self.todolist).delete(todo)
for todo in new_todos:
self.todolist.add_todo(todo)
self.out(self.printer.print_todo(todo))
else: else:
self._process_flags() self.error('Number of edited todos is not equal to '
'number of supplied todo IDs.')
if self.edit_archive: else:
archive = config().archive() self.error(self.usage())
return call([editor, archive]) == 0 def _execute_not_multi(self):
if self.edit_archive:
if self.is_expression: archive = config().archive()
self.todos = self._view().todos
else: return self._open_in_editor(archive) == 0
self.get_todos(self.args) else:
todo = config().todotxt()
todo_errors = self._catch_todo_errors()
return self._open_in_editor(todo) == 0
if not todo_errors:
temp_todos = self._todos_to_temp()
if not self._open_in_editor(temp_todos, editor):
new_todos = self._todos_from_temp(temp_todos)
if len(new_todos) == len(self.todos):
for todo in self.todos:
BASE_TODOLIST(self.todolist).delete(todo)
for todo in new_todos:
self.todolist.add_todo(todo)
self.out(self.printer.print_todo(todo))
else:
self.error('Number of edited todos is not equal to '
'number of supplied todo IDs.')
else:
self.error(self.usage())
else:
for error in todo_errors:
self.error(error)
except(OSError):
self.error('There is no such editor as: ' + editor + '. '
'Check your $EDITOR and/or $PATH')
def usage(self): def usage(self):
return """Synopsis: return """Synopsis:
......
...@@ -46,7 +46,7 @@ class ListCommand(ExpressionCommand): ...@@ -46,7 +46,7 @@ class ListCommand(ExpressionCommand):
""" """
try: try:
import icalendar as _ import icalendar as _
except ImportError: except ImportError: # pragma: no cover
self.error("icalendar package is not installed.") self.error("icalendar package is not installed.")
return False return False
...@@ -78,7 +78,7 @@ class ListCommand(ExpressionCommand): ...@@ -78,7 +78,7 @@ class ListCommand(ExpressionCommand):
Prints the todos in the right format. Prints the todos in the right format.
Defaults to normal text output (with possible colors and other pretty Defaults to normal text output (with possible colors and other pretty
printing. If a format was specified on the commandline, this format is printing). If a format was specified on the commandline, this format is
sent to the output. sent to the output.
""" """
...@@ -101,7 +101,7 @@ class ListCommand(ExpressionCommand): ...@@ -101,7 +101,7 @@ class ListCommand(ExpressionCommand):
try: try:
self._process_flags() self._process_flags()
except SyntaxError: except SyntaxError: # pragma: no cover
# importing icalendar failed, most likely due to Python 3.2 # importing icalendar failed, most likely due to Python 3.2
self.error("icalendar is not supported in this Python version.") self.error("icalendar is not supported in this Python version.")
return False return False
......
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
from datetime import date, timedelta from datetime import date, timedelta
from topydo.lib.MultiCommand import MultiCommand from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.Command import InvalidCommandArgument
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.RelativeDate import relative_date_to_date from topydo.lib.RelativeDate import relative_date_to_date
...@@ -32,19 +31,16 @@ class PostponeCommand(MultiCommand): ...@@ -32,19 +31,16 @@ class PostponeCommand(MultiCommand):
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
self._process_flags() self.last_argument = True
self.get_todos(self.args[:-1])
def _process_flags(self): def get_flags(self):
opts, args = self.getopt('s') return("s", [])
for opt, _ in opts: def process_flag(self, p_opt, p_value):
if opt == '-s': if p_opt == '-s':
self.move_start_date = True self.move_start_date = True
self.args = args def _execute_multi_specific(self):
def execute_multi_specific(self):
def _get_offset(p_todo): def _get_offset(p_todo):
offset = p_todo.tag_value( offset = p_todo.tag_value(
config().tag_due(), date.today().isoformat()) config().tag_due(), date.today().isoformat())
...@@ -55,34 +51,34 @@ class PostponeCommand(MultiCommand): ...@@ -55,34 +51,34 @@ class PostponeCommand(MultiCommand):
return offset_date return offset_date
try: pattern = self.args[-1]
pattern = self.args[-1] self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
for todo in self.todos:
offset = _get_offset(todo)
new_due = relative_date_to_date(pattern, offset)
if new_due: for todo in self.todos:
if self.move_start_date and todo.has_tag(config().tag_start()): offset = _get_offset(todo)
length = todo.length() new_due = relative_date_to_date(pattern, offset)
new_start = new_due - timedelta(length)
# pylint: disable=E1103
todo.set_tag(config().tag_start(), new_start.isoformat())
if new_due:
if self.move_start_date and todo.has_tag(config().tag_start()):
length = todo.length()
new_start = new_due - timedelta(length)
# pylint: disable=E1103 # pylint: disable=E1103
todo.set_tag(config().tag_due(), new_due.isoformat()) todo.set_tag(config().tag_start(), new_start.isoformat())
# pylint: disable=E1103
todo.set_tag(config().tag_due(), new_due.isoformat())
self.todolist.set_dirty() self.todolist.set_dirty()
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.")
break break
except (InvalidCommandArgument, IndexError):
self.error(self.usage())
def usage(self): def usage(self):
return "Synopsis: postpone [-s] <NUMBER> [<NUMBER2> ...] <PATTERN>" return """\
Synopsis: postpone [-s] <NUMBER> [<NUMBER2> ...] <PATTERN>"
postpone [-x] -e <EXPRESSION>
"""
def help(self): def help(self):
return """\ return """\
...@@ -91,6 +87,10 @@ Postpone the todo item(s) with the given number(s) and the given pattern. ...@@ -91,6 +87,10 @@ 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 flag is Postponing is done by adjusting the due date(s) of the todo(s), and if the -s flag is
given, the start date accordingly. given, the start date accordingly.
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
(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 1w' postpones
......
...@@ -14,6 +14,8 @@ ...@@ -14,6 +14,8 @@
# 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/>.
import re
from topydo.lib.MultiCommand import MultiCommand from topydo.lib.MultiCommand import MultiCommand
from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers from topydo.lib.PrettyPrinterFilter import PrettyPrinterNumbers
from topydo.lib.Utils import is_valid_priority from topydo.lib.Utils import is_valid_priority
...@@ -26,36 +28,42 @@ class PriorityCommand(MultiCommand): ...@@ -26,36 +28,42 @@ class PriorityCommand(MultiCommand):
super(PriorityCommand, self).__init__( super(PriorityCommand, self).__init__(
p_args, p_todolist, p_out, p_err, p_prompt) p_args, p_todolist, p_out, p_err, p_prompt)
self.get_todos(self.args[:-1]) self.last_argument = True
def execute_multi_specific(self): def _execute_multi_specific(self):
priority = None def normalize_priority(p_priority):
match = re.search(r'\b([A-Z])\b', p_priority)
return match.group(1) if match else p_priority
try: priority = normalize_priority(self.args[-1])
priority = self.args[-1] self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
if is_valid_priority(priority): if is_valid_priority(priority):
for todo in self.todos: for todo in self.todos:
old_priority = todo.priority() old_priority = todo.priority()
self.todolist.set_priority(todo, priority) self.todolist.set_priority(todo, priority)
if old_priority and priority and old_priority != priority: if old_priority and priority and old_priority != priority:
self.out("Priority changed from {} to {}".format( self.out("Priority changed from {} to {}".format(
old_priority, priority)) old_priority, priority))
elif not old_priority: elif not old_priority:
self.out("Priority set to {}.".format(priority)) self.out("Priority set to {}.".format(priority))
self.out(self.printer.print_todo(todo)) self.out(self.printer.print_todo(todo))
else: else:
self.error("Invalid priority given.") self.error("Invalid priority given.")
except IndexError:
self.error(self.usage())
def usage(self): def usage(self):
return """Synopsis: pri <NUMBER1> [<NUMBER2> ...] <PRIORITY>""" return """\
Synopsis: pri <NUMBER1> [<NUMBER2> ...] <PRIORITY>
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
the -e flag. Use -x to also process todo items that are normally invisible
(with the 'ls' subcommand).
""" """
...@@ -39,9 +39,7 @@ class SortCommand(Command): ...@@ -39,9 +39,7 @@ class SortCommand(Command):
sorted_todos = sorter.sort(self.todolist.todos()) sorted_todos = sorter.sort(self.todolist.todos())
self.todolist.erase() self.todolist.erase()
self.todolist.add_todos(sorted_todos)
for todo in sorted_todos:
self.todolist.add_todo(todo)
def usage(self): def usage(self):
return """Synopsis: sort [expression]""" return """Synopsis: sort [expression]"""
...@@ -51,8 +49,13 @@ class SortCommand(Command): ...@@ -51,8 +49,13 @@ class SortCommand(Command):
Sorts the file according to the expression. If no expression is given, the Sorts the file according to the expression. If no expression is given, the
expression in the configuration is used. expression in the configuration is used.
The following sort properties are supported: 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
precedence, then the second, etc.
The following sort attributes are supported:
* priority - Sort by priority
* creation - Sort by creation date * creation - Sort by creation date
* completed - Sort by completion state * completed - Sort by completion state
* importance - Sort by importance * importance - Sort by importance
...@@ -60,6 +63,10 @@ The following sort properties are supported: ...@@ -60,6 +63,10 @@ The following sort properties are supported:
* text - Sort by text * text - Sort by text
* <tag> - Sort by values of the given tag * <tag> - Sort by values of the given tag
Any property can be prefixed with 'asc:' and 'desc:' to specify the sort order. Each item can optionally be prefixed with asc: and desc: to specify ascending
The default is ascending sort. or descending sort respectively. If not specified, ascending sort is assumed.
Example:
desc:importance,due,desc:priority
""" """
...@@ -84,8 +84,10 @@ class Command(object): ...@@ -84,8 +84,10 @@ class Command(object):
return result return result
def usage(self): def usage(self):
return "No usage text available for this command." """ Returns a one-line synopsis for this command. """
raise NotImplementedError
def help(self): def help(self):
return "No help text available for this command." """ Returns the help text for this command. """
raise NotImplementedError
...@@ -38,7 +38,15 @@ class _Config: ...@@ -38,7 +38,15 @@ class _Config:
(such as todo.txt location passed with -t). The key is a tuple of (such as todo.txt location passed with -t). The key is a tuple of
(section, option), the value is the option's value. (section, option), the value is the option's value.
""" """
self.sections = ['topydo', 'tags', 'sort', 'ls', 'dep', 'colorscheme'] self.sections = [
'add',
'colorscheme',
'dep',
'ls',
'sort',
'tags',
'topydo',
]
self.defaults = { self.defaults = {
# topydo # topydo
...@@ -48,6 +56,9 @@ class _Config: ...@@ -48,6 +56,9 @@ class _Config:
'archive_filename' : 'done.txt', 'archive_filename' : 'done.txt',
'identifiers': 'linenumber', 'identifiers': 'linenumber',
# add
'auto_creation_date': '1',
# ls # ls
'hide_tags': 'id,p,ical', 'hide_tags': 'id,p,ical',
'indent': 0, 'indent': 0,
...@@ -185,6 +196,7 @@ class _Config: ...@@ -185,6 +196,7 @@ class _Config:
def hidden_tags(self): def hidden_tags(self):
""" Returns a list of tags to be hidden from the 'ls' output. """ """ Returns a list of tags to be hidden from the 'ls' output. """
hidden_tags = self.cp.get('ls', 'hide_tags') hidden_tags = self.cp.get('ls', 'hide_tags')
# pylint: disable=no-member
return [] if hidden_tags == '' else hidden_tags.split(',') return [] if hidden_tags == '' else hidden_tags.split(',')
def priority_colors(self): def priority_colors(self):
...@@ -233,6 +245,12 @@ class _Config: ...@@ -233,6 +245,12 @@ class _Config:
except ValueError: except ValueError:
return int(self.defaults['link_color']) return int(self.defaults['link_color'])
def auto_creation_date(self):
try:
return self.cp.getboolean('add', 'auto_creation_date')
except ValueError:
return self.defaults['auto_creation_date'] == '1'
def config(p_path=None, p_overrides=None): def config(p_path=None, p_overrides=None):
""" """
Retrieve the config instance. Retrieve the config instance.
......
...@@ -35,29 +35,14 @@ class DCommand(MultiCommand): ...@@ -35,29 +35,14 @@ class DCommand(MultiCommand):
self.force = False self.force = False
self.process_flags()
self.length = len(self.todolist.todos()) # to determine newly activated todos self.length = len(self.todolist.todos()) # to determine newly activated todos
self.get_todos(self.args)
def get_flags(self): def get_flags(self):
""" Default implementation of getting specific flags. """ return ("f", ["force"])
return ("", [])
def process_flag(self, p_option, p_value): def process_flag(self, p_opt, p_value):
""" Default implementation of processing specific flags. """ if p_opt == "-f" or p_opt == "--force":
pass self.force = True
def process_flags(self):
opts, args = self.get_flags()
opts, args = self.getopt("f" + opts, ["force"] + args)
for opt, value in opts:
if opt == "-f" or opt == "--force":
self.force = True
else:
self.process_flag(opt, value)
self.args = args
def _uncompleted_children(self, p_todo): def _uncompleted_children(self, p_todo):
return sorted( return sorted(
...@@ -71,11 +56,10 @@ class DCommand(MultiCommand): ...@@ -71,11 +56,10 @@ class DCommand(MultiCommand):
self.out(printer.print_list(p_todos)) self.out(printer.print_list(p_todos))
def prompt_text(self): def prompt_text(self):
return "Yes or no? [y/N] " raise NotImplementedError
def prefix(self): def prefix(self):
""" Prefix to use when printing a todo. """ raise NotImplementedError
return ""
def _process_subtasks(self, p_todo): def _process_subtasks(self, p_todo):
children = self._uncompleted_children(p_todo) children = self._uncompleted_children(p_todo)
...@@ -116,19 +100,19 @@ class DCommand(MultiCommand): ...@@ -116,19 +100,19 @@ class DCommand(MultiCommand):
return True return True
def condition_failed_text(self): def condition_failed_text(self):
return "" raise NotImplementedError
def execute_specific(self, _): def execute_specific(self, _):
pass raise NotImplementedError
def execute_specific_core(self, p_todo): def execute_specific_core(self, p_todo):
""" """
The core operation on the todo itself. Also used to operate on The core operation on the todo itself. Also used to operate on
child/parent tasks. child/parent tasks.
""" """
pass raise NotImplementedError
def execute_multi_specific(self): def _execute_multi_specific(self):
old_active = self._active_todos() old_active = self._active_todos()
for todo in self.todos: for todo in self.todos:
......
...@@ -35,6 +35,9 @@ class ExpressionCommand(Command): ...@@ -35,6 +35,9 @@ class ExpressionCommand(Command):
self.sort_expression = config().sort_string() self.sort_expression = config().sort_string()
self.show_all = False self.show_all = False
# Commands using last argument differently (i.e as something other than
# todo ID/expression) have to set attribute below to True.
self.last_argument = False
def _filters(self): def _filters(self):
filters = [] filters = []
...@@ -43,7 +46,8 @@ class ExpressionCommand(Command): ...@@ -43,7 +46,8 @@ class ExpressionCommand(Command):
filters.append(Filter.DependencyFilter(self.todolist)) filters.append(Filter.DependencyFilter(self.todolist))
filters.append(Filter.RelevanceFilter()) filters.append(Filter.RelevanceFilter())
filters += Filter.get_filter_list(self.args) 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(config().list_limit())) filters.append(Filter.LimitFilter(config().list_limit()))
......
...@@ -29,8 +29,7 @@ class Filter(object): ...@@ -29,8 +29,7 @@ class Filter(object):
return [t for t in p_todos if self.match(t)] return [t for t in p_todos if self.match(t)]
def match(self, _): def match(self, _):
""" Default match value. """ raise NotImplementedError
return True
class NegationFilter(Filter): class NegationFilter(Filter):
def __init__(self, p_filter): def __init__(self, p_filter):
...@@ -157,13 +156,28 @@ ORDINAL_TAG_MATCH = r"(?P<key>[^:]*):(?P<operator><=?|=|>=?|!)?(?P<value>\S+)" ...@@ -157,13 +156,28 @@ ORDINAL_TAG_MATCH = r"(?P<key>[^:]*):(?P<operator><=?|=|>=?|!)?(?P<value>\S+)"
class OrdinalTagFilter(Filter): class OrdinalTagFilter(Filter):
def __init__(self, p_expression): def __init__(self, p_expression):
super(OrdinalTagFilter, self).__init__() super(OrdinalTagFilter, self).__init__()
match = re.match(ORDINAL_TAG_MATCH, p_expression)
self.expression = p_expression
match = re.match(ORDINAL_TAG_MATCH, self.expression)
if match: if match:
self.key = match.group('key') self.key = match.group('key')
self.operator = match.group('operator') or '=' self.operator = match.group('operator') or '='
self.value = match.group('value') self.value = match.group('value')
def match(self, p_todo): def match(self, p_todo):
"""
Performs a match on a key:value tag in the todo.
First it tries to convert the value and the user-entered expression to
a date and makes a comparison if it succeeds, based on the given
operator (default ==).
Upon failure, it falls back to converting value and user-entered
expression to an integer and makes a numerical comparison based on the
given operator (default ==)
As a last resort, it falls back to using a Grep filter to see if the
user given expression is contained in the todo text.
"""
if not self.key or not p_todo.has_tag(self.key): if not self.key or not p_todo.has_tag(self.key):
return False return False
...@@ -175,11 +189,15 @@ class OrdinalTagFilter(Filter): ...@@ -175,11 +189,15 @@ class OrdinalTagFilter(Filter):
operand2 = date_string_to_date(self.value) operand2 = date_string_to_date(self.value)
except ValueError: except ValueError:
operand1 = p_todo.tag_value(self.key)
operand2 = self.value
try: try:
operand1 = int(p_todo.tag_value(self.key)) operand1 = int(operand1)
operand2 = int(self.value) operand2 = int(operand2)
except ValueError: except ValueError:
return False grep = GrepFilter(self.expression)
return grep.match(p_todo)
if self.operator == '<': if self.operator == '<':
return operand1 < operand2 return operand1 < operand2
......
...@@ -69,15 +69,12 @@ class IcalPrinter(Printer): ...@@ -69,15 +69,12 @@ class IcalPrinter(Printer):
try: try:
import icalendar import icalendar
self.icalendar = icalendar self.icalendar = icalendar
except (SyntaxError, ImportError): except (SyntaxError, ImportError): # pragma: no cover
# icalendar does not support Python 3.2 resulting in a SyntaxError. Since # icalendar does not support Python 3.2 resulting in a SyntaxError. Since
# this is an optional dependency, dropping Python 3.2 support altogether is # this is an optional dependency, dropping Python 3.2 support altogether is
# too much. Therefore just disable the iCalendar functionality # too much. Therefore just disable the iCalendar functionality
self.icalendar = None self.icalendar = None
def print_todo(self, p_todo):
return self._convert_todo(p_todo).to_ical() if self.icalendar else ""
def print_list(self, p_todos): def print_list(self, p_todos):
result = "" result = ""
......
...@@ -16,10 +16,10 @@ ...@@ -16,10 +16,10 @@
from six import u from six import u
from topydo.lib.Command import Command from topydo.lib.ExpressionCommand import ExpressionCommand
from topydo.lib.TodoListBase import InvalidTodoException from topydo.lib.TodoListBase import InvalidTodoException
class MultiCommand(Command): class MultiCommand(ExpressionCommand):
""" """
A common class for operations that can work with multiple todo IDs. A common class for operations that can work with multiple todo IDs.
""" """
...@@ -33,14 +33,49 @@ class MultiCommand(Command): ...@@ -33,14 +33,49 @@ class MultiCommand(Command):
self.todos = [] self.todos = []
self.invalid_numbers = [] self.invalid_numbers = []
self.is_expression = False
self.multi_mode = True
def get_todos(self, p_numbers): def get_flags(self):
""" Default implementation of getting specific flags. """
return ("", [])
def process_flag(self, p_option, p_value):
""" Default implementation of processing specific flags. """
pass
def _process_flags(self):
opts, long_opts = self.get_flags()
opts, args = self.getopt("xe" + opts, long_opts)
for opt, value in opts:
if opt == '-x':
self.show_all = True
elif opt == '-e':
self.is_expression = True
else:
self.process_flag(opt, value)
self.args = args
def get_todos_from_expr(self):
self.todos = self._view().todos
def get_todos(self):
""" Gets todo objects from supplied todo IDs """ """ Gets todo objects from supplied todo IDs """
for number in p_numbers: if self.is_expression:
try: self.get_todos_from_expr()
self.todos.append(self.todolist.todo(number)) else:
except InvalidTodoException: if self.last_argument:
self.invalid_numbers.append(number) numbers = self.args[:-1]
else:
numbers = self.args
for number in numbers:
try:
self.todos.append(self.todolist.todo(number))
except InvalidTodoException:
self.invalid_numbers.append(number)
def _catch_todo_errors(self): def _catch_todo_errors(self):
""" """
...@@ -65,23 +100,36 @@ class MultiCommand(Command): ...@@ -65,23 +100,36 @@ class MultiCommand(Command):
else: else:
return None return None
def execute_multi_specific(self): def _execute_multi_specific(self):
""" """
Operations specific for particular command dealing with multiple todo Operations specific for particular command dealing with multiple todo
IDs. IDs.
""" """
pass pass
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
def execute(self): def execute(self):
if not super(MultiCommand, self).execute(): if not super(MultiCommand, self).execute():
return False return False
todo_errors = self._catch_todo_errors() self._process_flags()
if not todo_errors: if not self.multi_mode:
self.execute_multi_specific() self._execute_not_multi()
else: else:
for error in todo_errors: self.get_todos()
self.error(error) todo_errors = self._catch_todo_errors()
if not todo_errors:
self._execute_multi_specific()
else:
for error in todo_errors:
self.error(error)
return True return True
...@@ -26,8 +26,7 @@ class Printer(object): ...@@ -26,8 +26,7 @@ class Printer(object):
Subclasses must at least implement the print_todo method. Subclasses must at least implement the print_todo method.
""" """
def print_todo(self, p_todo): def print_todo(self, p_todo):
""" Base implementation.""" raise NotImplementedError
return p_todo.source()
def print_list(self, p_todos): def print_list(self, p_todos):
""" """
......
...@@ -30,8 +30,10 @@ class PrettyPrinterFilter(object): ...@@ -30,8 +30,10 @@ class PrettyPrinterFilter(object):
""" """
def filter(self, p_todo_str, _): def filter(self, p_todo_str, _):
""" Default implementation returns an unmodified todo string. """ """
return p_todo_str Applies a filter to p_todo_str and returns a modified version of it.
"""
raise NotImplementedError
class PrettyPrinterColorFilter(PrettyPrinterFilter): class PrettyPrinterColorFilter(PrettyPrinterFilter):
""" """
...@@ -69,7 +71,7 @@ class PrettyPrinterColorFilter(PrettyPrinterFilter): ...@@ -69,7 +71,7 @@ class PrettyPrinterColorFilter(PrettyPrinterFilter):
p_todo_str) p_todo_str)
# tags # tags
p_todo_str = re.sub(r'\b\S+:[^/\s]\S+\b', p_todo_str = re.sub(r'\b\S+:[^/\s]\S*\b',
metadata_color + r'\g<0>' + color, metadata_color + r'\g<0>' + color,
p_todo_str) p_todo_str)
...@@ -78,7 +80,7 @@ class PrettyPrinterColorFilter(PrettyPrinterFilter): ...@@ -78,7 +80,7 @@ class PrettyPrinterColorFilter(PrettyPrinterFilter):
' ' + link_color + r'\2\3' + color, ' ' + link_color + r'\2\3' + color,
p_todo_str) p_todo_str)
p_todo_str += NEUTRAL_COLOR p_todo_str += NEUTRAL_COLOR
return p_todo_str return p_todo_str
......
...@@ -25,24 +25,36 @@ from topydo.lib.Todo import Todo ...@@ -25,24 +25,36 @@ from topydo.lib.Todo import Todo
class NoRecurrenceException(Exception): class NoRecurrenceException(Exception):
pass pass
def _advance_recurring_todo_helper(p_todo, p_offset): def advance_recurring_todo(p_todo, p_offset=None, p_strict=False):
""" """
Given a Todo item, return a new instance of a Todo item with the dates Given a Todo item, return a new instance of a Todo item with the dates
shifted according to the recurrence rule. shifted according to the recurrence rule.
The new date is calculated from the given p_offset value. Strict means that the real due date is taken as a offset, not today or a
future date to determine the offset.
When the todo item has no due date, then the date is used passed by the
caller (defaulting to today).
When no recurrence tag is present, an exception is raised. When no recurrence tag is present, an exception is raised.
""" """
todo = Todo(p_todo.source()) todo = Todo(p_todo.source())
pattern = todo.tag_value('rec') pattern = todo.tag_value('rec')
if not pattern: if not pattern:
raise NoRecurrenceException() raise NoRecurrenceException()
elif pattern.startswith('+'):
p_strict = True
# strip off the +
pattern = pattern[1:]
if p_strict:
offset = p_todo.due_date() or p_offset or date.today()
else:
offset = p_offset or date.today()
length = todo.length() length = todo.length()
new_due = relative_date_to_date(pattern, p_offset) new_due = relative_date_to_date(pattern, offset)
if not new_due: if not new_due:
raise NoRecurrenceException() raise NoRecurrenceException()
...@@ -57,22 +69,3 @@ def _advance_recurring_todo_helper(p_todo, p_offset): ...@@ -57,22 +69,3 @@ def _advance_recurring_todo_helper(p_todo, p_offset):
todo.set_creation_date(date.today()) todo.set_creation_date(date.today())
return todo return todo
def advance_recurring_todo(p_todo, p_offset=date.today()):
return _advance_recurring_todo_helper(p_todo, p_offset)
def strict_advance_recurring_todo(p_todo, p_offset=date.today()):
"""
Given a Todo item, return a new instance of a Todo item with the dates
shifted according to the recurrence rule.
Strict means that the real due date is taken as a offset, not today or a
future date to determine the offset.
When the todo item has no due date, then the date is used passed by the
caller (defaulting to today).
When no recurrence tag is present, an exception is raised.
"""
offset = p_todo.due_date() or p_offset
return _advance_recurring_todo_helper(p_todo, offset)
...@@ -35,13 +35,14 @@ def _add_months(p_sourcedate, p_months): ...@@ -35,13 +35,14 @@ def _add_months(p_sourcedate, p_months):
return date(year, month, day) return date(year, month, day)
def _convert_pattern(p_length, p_periodunit, p_offset=date.today()): def _convert_pattern(p_length, p_periodunit, p_offset=None):
""" """
Converts a pattern in the form [0-9][dwmy] and returns a date from today Converts a pattern in the form [0-9][dwmy] and returns a date from the
with the period of time added to it. offset with the period of time added to it.
""" """
result = None result = None
p_offset = p_offset or date.today()
p_length = int(p_length) p_length = int(p_length)
if p_periodunit == 'd': if p_periodunit == 'd':
...@@ -80,7 +81,7 @@ def _convert_weekday_pattern(p_weekday): ...@@ -80,7 +81,7 @@ def _convert_weekday_pattern(p_weekday):
shift = (target_day - day) % 7 shift = (target_day - day) % 7
return date.today() + timedelta(shift) return date.today() + timedelta(shift)
def relative_date_to_date(p_date, p_offset=date.today()): def relative_date_to_date(p_date, p_offset=None):
""" """
Transforms a relative date into a date object. Transforms a relative date into a date object.
...@@ -93,6 +94,7 @@ def relative_date_to_date(p_date, p_offset=date.today()): ...@@ -93,6 +94,7 @@ def relative_date_to_date(p_date, p_offset=date.today()):
result = None result = None
p_date = p_date.lower() p_date = p_date.lower()
p_offset = p_offset or date.today()
relative = re.match('(?P<length>-?[0-9]+)(?P<period>[dwmy])$', p_date, re.I) relative = re.match('(?P<length>-?[0-9]+)(?P<period>[dwmy])$', p_date, re.I)
......
...@@ -36,14 +36,11 @@ class TodoList(TodoListBase): ...@@ -36,14 +36,11 @@ 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._todos = [] # 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._depgraph = DirectedGraph()
self._todo_id_map = {}
self._id_todo_map = {}
self.add_list(p_todostrings) super(TodoList, self).__init__(p_todostrings)
self.dirty = False
def todo_by_dep_id(self, p_dep_id): def todo_by_dep_id(self, p_dep_id):
""" """
......
""" Version of Topydo. """ """ Version of Topydo. """
VERSION = '0.4' VERSION = '0.6'
LICENSE = """Copyright (C) 2014 Bram Schoenmakers LICENSE = """Copyright (C) 2014 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>.
......
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