Commit 7838a8ba authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge branch 'master' into ls-n

parents 76a38dcc 10ec25eb
...@@ -5,10 +5,6 @@ smoothly into topydo. ...@@ -5,10 +5,6 @@ smoothly into topydo.
### General ### General
* 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
Request on the `master` branch.
* Use descriptive commit messages. The post * Use descriptive commit messages. The post
[How to write a commit message](http://chris.beams.io/posts/git-commit/) by [How to write a commit message](http://chris.beams.io/posts/git-commit/) by
Chris Beams has some good guidelines. Chris Beams has some good guidelines.
......
...@@ -43,6 +43,6 @@ Demo ...@@ -43,6 +43,6 @@ Demo
[2]: https://github.com/ginatrapani/todo.txt-cli [2]: https://github.com/ginatrapani/todo.txt-cli
[3]: https://github.com/bram85/todo.txt-tools [3]: https://github.com/bram85/todo.txt-tools
[4]: https://github.com/bram85/topydo/wiki [4]: https://github.com/bram85/topydo/wiki
[5]: https://raw.githubusercontent.com/bram85/topydo/stable/doc/topydo.gif [5]: https://raw.githubusercontent.com/bram85/topydo/master/doc/topydo.gif
[6]: https://github.com/jonathanslenders/python-prompt-toolkit [6]: https://github.com/jonathanslenders/python-prompt-toolkit
[7]: https://github.com/collective/icalendar [7]: https://github.com/collective/icalendar
...@@ -45,24 +45,30 @@ class EditCommandTest(CommandTest): ...@@ -45,24 +45,30 @@ class EditCommandTest(CommandTest):
self.todolist = TodoList(todos) self.todolist = TodoList(todos)
@mock.patch('topydo.commands.EditCommand._is_edited')
@mock.patch('topydo.commands.EditCommand.EditCommand._todos_from_temp')
@mock.patch('topydo.commands.EditCommand.EditCommand._open_in_editor') @mock.patch('topydo.commands.EditCommand.EditCommand._open_in_editor')
def test_edit1(self, mock_open_in_editor): def test_edit01(self, mock_open_in_editor, mock_todos_from_temp, mock_is_edited):
""" Preserve dependencies after editing. """ """ Preserve dependencies after editing. """
mock_open_in_editor.return_value = 0 mock_open_in_editor.return_value = 0
mock_todos_from_temp.return_value = [Todo('Foo id:1')]
mock_is_edited.return_value = True
command = EditCommand(["1"], self.todolist, self.out, self.error, None) command = EditCommand(["1"], self.todolist, self.out, self.error, None)
command.execute() command.execute()
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.todolist.print_todos(), u("Bar p:1 @test\nBaz @test\nFo\u00f3B\u0105\u017a\nFoo id:1")) self.assertEqual(self.todolist.print_todos(), u("Bar p:1 @test\nBaz @test\nFo\u00f3B\u0105\u017a\nFoo id:1"))
@mock.patch('topydo.commands.EditCommand._is_edited')
@mock.patch('topydo.commands.EditCommand.EditCommand._todos_from_temp') @mock.patch('topydo.commands.EditCommand.EditCommand._todos_from_temp')
@mock.patch('topydo.commands.EditCommand.EditCommand._open_in_editor') @mock.patch('topydo.commands.EditCommand.EditCommand._open_in_editor')
def test_edit2(self, mock_open_in_editor, mock_todos_from_temp): def test_edit02(self, mock_open_in_editor, mock_todos_from_temp, mock_is_edited):
""" Edit some todo. """ """ Edit some todo. """
mock_open_in_editor.return_value = 0 mock_open_in_editor.return_value = 0
mock_todos_from_temp.return_value = [Todo('Lazy Cat')] mock_todos_from_temp.return_value = [Todo('Lazy Cat')]
mock_is_edited.return_value = True
command = EditCommand(["Bar"], self.todolist, self.out, self.error, command = EditCommand(["Bar"], self.todolist, self.out, self.error,
None) None)
...@@ -72,7 +78,7 @@ class EditCommandTest(CommandTest): ...@@ -72,7 +78,7 @@ class EditCommandTest(CommandTest):
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
self.assertEqual(self.todolist.print_todos(), u("Foo id:1\nBaz @test\nFo\u00f3B\u0105\u017a\nLazy Cat")) self.assertEqual(self.todolist.print_todos(), u("Foo id:1\nBaz @test\nFo\u00f3B\u0105\u017a\nLazy Cat"))
def test_edit3(self): def test_edit03(self):
""" Throw an error after invalid todo number given as argument. """ """ Throw an error after invalid todo number given as argument. """
command = EditCommand(["FooBar"], self.todolist, self.out, self.error, command = EditCommand(["FooBar"], self.todolist, self.out, self.error,
None) None)
...@@ -81,7 +87,7 @@ class EditCommandTest(CommandTest): ...@@ -81,7 +87,7 @@ class EditCommandTest(CommandTest):
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.errors, "Invalid todo number given.\n") self.assertEqual(self.errors, "Invalid todo number given.\n")
def test_edit4(self): def test_edit04(self):
""" Throw an error with pointing invalid argument. """ """ Throw an error with pointing invalid argument. """
command = EditCommand(["Bar", "5"], self.todolist, self.out, command = EditCommand(["Bar", "5"], self.todolist, self.out,
self.error, None) self.error, None)
...@@ -90,22 +96,7 @@ class EditCommandTest(CommandTest): ...@@ -90,22 +96,7 @@ class EditCommandTest(CommandTest):
self.assertFalse(self.todolist.is_dirty()) self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.errors, "Invalid todo number given: 5.\n") self.assertEqual(self.errors, "Invalid todo number given: 5.\n")
@mock.patch('topydo.commands.EditCommand.EditCommand._todos_from_temp') def test_edit05(self):
@mock.patch('topydo.commands.EditCommand.EditCommand._open_in_editor')
def test_edit5(self, mock_open_in_editor, mock_todos_from_temp):
""" Don't let to delete todos acidentally while editing. """
mock_open_in_editor.return_value = 0
mock_todos_from_temp.return_value = [Todo('Only one line')]
command = EditCommand(["1", "Bar"], self.todolist, self.out,
self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.errors, "Number of edited todos is not equal to number of supplied todo IDs.\n")
self.assertEqual(self.todolist.print_todos(), u("Foo id:1\nBar p:1 @test\nBaz @test\nFo\u00f3B\u0105\u017a"))
def test_edit6(self):
""" """
Throw an error with invalid argument containing special characters. Throw an error with invalid argument containing special characters.
""" """
...@@ -117,12 +108,14 @@ class EditCommandTest(CommandTest): ...@@ -117,12 +108,14 @@ class EditCommandTest(CommandTest):
self.assertEqual(self.errors, self.assertEqual(self.errors,
u("Invalid todo number given: Fo\u00d3B\u0105r.\n")) u("Invalid todo number given: Fo\u00d3B\u0105r.\n"))
@mock.patch('topydo.commands.EditCommand._is_edited')
@mock.patch('topydo.commands.EditCommand.EditCommand._todos_from_temp') @mock.patch('topydo.commands.EditCommand.EditCommand._todos_from_temp')
@mock.patch('topydo.commands.EditCommand.EditCommand._open_in_editor') @mock.patch('topydo.commands.EditCommand.EditCommand._open_in_editor')
def test_edit7(self, mock_open_in_editor, mock_todos_from_temp): def test_edit06(self, mock_open_in_editor, mock_todos_from_temp, mock_is_edited):
""" Edit todo with special characters. """ """ Edit todo with special characters. """
mock_open_in_editor.return_value = 0 mock_open_in_editor.return_value = 0
mock_todos_from_temp.return_value = [Todo('Lazy Cat')] mock_todos_from_temp.return_value = [Todo('Lazy Cat')]
mock_is_edited.return_value = True
command = EditCommand([u("Fo\u00f3B\u0105\u017a")], self.todolist, command = EditCommand([u("Fo\u00f3B\u0105\u017a")], self.todolist,
self.out, self.error, None) self.out, self.error, None)
...@@ -133,13 +126,32 @@ class EditCommandTest(CommandTest): ...@@ -133,13 +126,32 @@ class EditCommandTest(CommandTest):
self.assertEqual(self.todolist.print_todos(), self.assertEqual(self.todolist.print_todos(),
u("Foo id:1\nBar p:1 @test\nBaz @test\nLazy Cat")) u("Foo id:1\nBar p:1 @test\nBaz @test\nLazy Cat"))
@mock.patch('topydo.commands.EditCommand._is_edited')
@mock.patch('topydo.commands.EditCommand.EditCommand._todos_from_temp') @mock.patch('topydo.commands.EditCommand.EditCommand._todos_from_temp')
@mock.patch('topydo.commands.EditCommand.EditCommand._open_in_editor') @mock.patch('topydo.commands.EditCommand.EditCommand._open_in_editor')
def test_edit_expr(self, mock_open_in_editor, mock_todos_from_temp): def test_edit07(self, mock_open_in_editor, mock_todos_from_temp, mock_is_edited):
""" Don't perform write if tempfile is unchanged """
mock_open_in_editor.return_value = 0
mock_todos_from_temp.return_value = [Todo('Only one line')]
mock_is_edited.return_value = False
command = EditCommand(["1", "Bar"], self.todolist, self.out,
self.error, None)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.errors, "Editing aborted. Nothing to do.\n")
self.assertEqual(self.todolist.print_todos(), u("Foo id:1\nBar p:1 @test\nBaz @test\nFo\u00f3B\u0105\u017a"))
@mock.patch('topydo.commands.EditCommand._is_edited')
@mock.patch('topydo.commands.EditCommand.EditCommand._todos_from_temp')
@mock.patch('topydo.commands.EditCommand.EditCommand._open_in_editor')
def test_edit_expr(self, mock_open_in_editor, mock_todos_from_temp, mock_is_edited):
""" Edit todos matching expression. """ """ Edit todos matching expression. """
mock_open_in_editor.return_value = 0 mock_open_in_editor.return_value = 0
mock_todos_from_temp.return_value = [Todo('Lazy Cat'), mock_todos_from_temp.return_value = [Todo('Lazy Cat'),
Todo('Lazy Dog')] Todo('Lazy Dog')]
mock_is_edited.return_value = True
command = EditCommand(["-e", "@test"], self.todolist, self.out, command = EditCommand(["-e", "@test"], self.todolist, self.out,
self.error, None) self.error, None)
...@@ -147,8 +159,8 @@ class EditCommandTest(CommandTest): ...@@ -147,8 +159,8 @@ class EditCommandTest(CommandTest):
expected = u("| 3| Lazy Cat\n| 4| Lazy Dog\n") expected = u("| 3| Lazy Cat\n| 4| Lazy Dog\n")
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.errors, "") self.assertEqual(self.errors, "")
self.assertTrue(self.todolist.is_dirty())
self.assertEqual(self.output, expected) self.assertEqual(self.output, expected)
self.assertEqual(self.todolist.print_todos(), u("Foo id:1\nFo\u00f3B\u0105\u017a\nLazy Cat\nLazy Dog")) self.assertEqual(self.todolist.print_todos(), u("Foo id:1\nFo\u00f3B\u0105\u017a\nLazy Cat\nLazy Dog"))
......
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
from six import u
from test.TopydoTestCase import TopydoTest
from topydo.Commands import get_subcommand
from topydo.commands.AddCommand import AddCommand
from topydo.commands.DeleteCommand import DeleteCommand
from topydo.commands.ListCommand import ListCommand
from topydo.commands.ListProjectCommand import ListProjectCommand
from topydo.lib.Config import config
class GetSubcommandTest(TopydoTest):
def test_normal_cmd(self):
args = ["add"]
real_cmd, final_args = get_subcommand(args)
self.assertTrue(issubclass(real_cmd, AddCommand))
def test_cmd_help(self):
args = ["help", "add"]
real_cmd, final_args = get_subcommand(args)
self.assertTrue(issubclass(real_cmd, AddCommand))
self.assertEqual(final_args, ["help"])
def test_alias(self):
config("test/data/aliases.conf")
args = ["foo"]
real_cmd, final_args = get_subcommand(args)
self.assertTrue(issubclass(real_cmd, DeleteCommand))
self.assertEqual(final_args, ["-f", "test"])
def test_default_cmd01(self):
args = ["bar"]
real_cmd, final_args = get_subcommand(args)
self.assertTrue(issubclass(real_cmd, ListCommand))
self.assertEqual(final_args, ["bar"])
def test_default_cmd02(self):
args = []
real_cmd, final_args = get_subcommand(args)
self.assertTrue(issubclass(real_cmd, ListCommand))
self.assertEqual(final_args, [])
def test_alias_default_cmd01(self):
config("test/data/aliases.conf", {('topydo', 'default_command'): 'foo'})
args = ["bar"]
real_cmd, final_args = get_subcommand(args)
self.assertTrue(issubclass(real_cmd, DeleteCommand))
self.assertEqual(final_args, ["-f", "test", "bar"])
def test_alias_default_cmd02(self):
config("test/data/aliases.conf", {('topydo', 'default_command'): 'foo'})
args = []
real_cmd, final_args = get_subcommand(args)
self.assertTrue(issubclass(real_cmd, DeleteCommand))
self.assertEqual(final_args, ["-f", "test"])
def test_wrong_alias(self):
config("test/data/aliases.conf")
args = ["baz"]
real_cmd, final_args = get_subcommand(args)
self.assertEqual(real_cmd, None)
if __name__ == '__main__':
unittest.main()
[aliases]
foo = rm -f test
baz = FooBar
...@@ -46,3 +46,14 @@ append_parent_contexts = 0 ...@@ -46,3 +46,14 @@ append_parent_contexts = 0
; context_color = magenta ; context_color = magenta
; metadata_color = green ; metadata_color = green
; link_color = light-cyan ; link_color = light-cyan
[aliases]
;showall = ls -x
;lsproj = lsprj
;listprj = lsprj
;listproj = lsprj
;listproject = lsprj
;listprojects = lsprj
;listcon = lscon
;listcontext = lscon
;listcontexts = lscon
...@@ -35,15 +35,7 @@ _SUBCOMMAND_MAP = { ...@@ -35,15 +35,7 @@ _SUBCOMMAND_MAP = {
'exit': 'ExitCommand', # used for the prompt 'exit': 'ExitCommand', # used for the prompt
'ls': 'ListCommand', 'ls': 'ListCommand',
'lscon': 'ListContextCommand', 'lscon': 'ListContextCommand',
'listcon': 'ListContextCommand',
'listcontext': 'ListContextCommand',
'listcontexts': 'ListContextCommand',
'lsprj': 'ListProjectCommand', 'lsprj': 'ListProjectCommand',
'lsproj': 'ListProjectCommand',
'listprj': 'ListProjectCommand',
'listproj': 'ListProjectCommand',
'listproject': 'ListProjectCommand',
'listprojects': 'ListProjectCommand',
'postpone': 'PostponeCommand', 'postpone': 'PostponeCommand',
'pri': 'PriorityCommand', 'pri': 'PriorityCommand',
'quit': 'ExitCommand', 'quit': 'ExitCommand',
...@@ -53,7 +45,6 @@ _SUBCOMMAND_MAP = { ...@@ -53,7 +45,6 @@ _SUBCOMMAND_MAP = {
'tag': 'TagCommand', 'tag': 'TagCommand',
} }
def get_subcommand(p_args): def get_subcommand(p_args):
""" """
Retrieves the to-be executed Command and returns a tuple (Command, args). Retrieves the to-be executed Command and returns a tuple (Command, args).
...@@ -80,13 +71,31 @@ def get_subcommand(p_args): ...@@ -80,13 +71,31 @@ def get_subcommand(p_args):
__import__(modulename, globals(), locals(), [classname], 0) __import__(modulename, globals(), locals(), [classname], 0)
return getattr(sys.modules[modulename], classname) return getattr(sys.modules[modulename], classname)
def resolve_alias(p_alias, p_args):
"""
Resolves a subcommand alias and returns a tuple (Command, args).
If alias resolves to non-existent command, main help message is
returned.
"""
real_subcommand, alias_args = alias_map[p_alias]
try:
result = import_subcommand(real_subcommand)
args = alias_args + p_args
return (result, args)
except KeyError:
return get_subcommand(['help'])
result = None result = None
args = p_args args = p_args
alias_map = config().aliases()
try: try:
subcommand = p_args[0] subcommand = p_args[0]
if subcommand in _SUBCOMMAND_MAP: if subcommand in alias_map:
result, args = resolve_alias(subcommand, args[1:])
elif subcommand in _SUBCOMMAND_MAP:
result = import_subcommand(subcommand) result = import_subcommand(subcommand)
args = args[1:] args = args[1:]
elif subcommand == 'help': elif subcommand == 'help':
...@@ -101,12 +110,16 @@ def get_subcommand(p_args): ...@@ -101,12 +110,16 @@ def get_subcommand(p_args):
pass pass
else: else:
p_command = config().default_command() p_command = config().default_command()
if p_command in _SUBCOMMAND_MAP: if p_command in alias_map:
result, args = resolve_alias(p_command, args)
elif p_command in _SUBCOMMAND_MAP:
result = import_subcommand(p_command) result = import_subcommand(p_command)
# leave args unchanged # leave args unchanged
except IndexError: except IndexError:
p_command = config().default_command() p_command = config().default_command()
if p_command in _SUBCOMMAND_MAP: if p_command in alias_map:
result, args = resolve_alias(p_command, args)
elif p_command in _SUBCOMMAND_MAP:
result = import_subcommand(p_command) result = import_subcommand(p_command)
return (result, args) return (result, args)
...@@ -101,12 +101,9 @@ except ConfigError as config_error: ...@@ -101,12 +101,9 @@ except ConfigError as config_error:
error(str(config_error)) error(str(config_error))
sys.exit(1) sys.exit(1)
from topydo.commands.ArchiveCommand import ArchiveCommand
from topydo.commands.SortCommand import SortCommand
from topydo.lib import TodoFile from topydo.lib import TodoFile
from topydo.lib import TodoList from topydo.lib import TodoList
from topydo.lib import TodoListBase from topydo.lib import TodoListBase
from topydo.lib.ChangeSet import ChangeSet
from topydo.lib.Utils import escape_ansi from topydo.lib.Utils import escape_ansi
...@@ -178,6 +175,7 @@ class CLIApplicationBase(object): ...@@ -178,6 +175,7 @@ class CLIApplicationBase(object):
self.backup.add_archive(archive) self.backup.add_archive(archive)
if archive: if archive:
from topydo.commands.ArchiveCommand import ArchiveCommand
command = ArchiveCommand(self.todolist, archive) command = ArchiveCommand(self.todolist, archive)
command.execute() command.execute()
...@@ -196,14 +194,21 @@ class CLIApplicationBase(object): ...@@ -196,14 +194,21 @@ class CLIApplicationBase(object):
""" """
return input return input
def is_read_only(self, p_command):
""" Returns True when the given command class is read-only. """
read_only_commands = tuple(cmd + 'Command' for cmd in ('Revert', ) +
READ_ONLY_COMMANDS)
return p_command.__module__.endswith(read_only_commands)
def _execute(self, p_command, p_args): def _execute(self, p_command, p_args):
""" """
Execute a subcommand with arguments. p_command is a class (not an Execute a subcommand with arguments. p_command is a class (not an
object). object).
""" """
cmds_wo_backup = tuple(cmd + 'Command' for cmd in ('Revert', ) + READ_ONLY_COMMANDS) if config().backup_count() > 0 and p_command and not self.is_read_only(p_command):
if config().backup_count() > 0 and p_command and not p_command.__module__.endswith(cmds_wo_backup):
call = [p_command.__module__.lower()[16:-7]] + p_args # strip "topydo.commands" and "Command" call = [p_command.__module__.lower()[16:-7]] + p_args # strip "topydo.commands" and "Command"
from topydo.lib.ChangeSet import ChangeSet
self.backup = ChangeSet(self.todolist, p_call=call) self.backup = ChangeSet(self.todolist, p_call=call)
command = p_command( command = p_command(
...@@ -232,6 +237,7 @@ class CLIApplicationBase(object): ...@@ -232,6 +237,7 @@ class CLIApplicationBase(object):
self._archive() self._archive()
if config().keep_sorted(): if config().keep_sorted():
from topydo.commands.SortCommand import SortCommand
self._execute(SortCommand, []) self._execute(SortCommand, [])
if self.backup: if self.backup:
......
...@@ -99,15 +99,14 @@ class PromptApplication(CLIApplicationBase): ...@@ -99,15 +99,14 @@ class PromptApplication(CLIApplicationBase):
sys.exit(0) sys.exit(0)
mtime_after = _todotxt_mtime() mtime_after = _todotxt_mtime()
(subcommand, args) = get_subcommand(user_input)
if self.mtime != mtime_after:
# refuse to perform operations such as 'del' and 'do' if the # refuse to perform operations such as 'del' and 'do' if the
# todo.txt file has been changed in the background. # todo.txt file has been changed in the background.
if not self.is_read_only(subcommand) and self.mtime != mtime_after:
error("WARNING: todo.txt file was modified by another application.\nTo prevent unintended changes, this operation was not executed.") error("WARNING: todo.txt file was modified by another application.\nTo prevent unintended changes, this operation was not executed.")
continue continue
(subcommand, args) = get_subcommand(user_input)
try: try:
if self._execute(subcommand, args) != False: if self._execute(subcommand, args) != False:
self._post_execute() self._post_execute()
......
...@@ -30,7 +30,9 @@ from topydo.lib.RelativeDate import relative_date_to_date ...@@ -30,7 +30,9 @@ from topydo.lib.RelativeDate import relative_date_to_date
def _subcommands(p_word_before_cursor): def _subcommands(p_word_before_cursor):
""" Generator for subcommand name completion. """ """ Generator for subcommand name completion. """
subcommands = [sc for sc in sorted(_SUBCOMMAND_MAP.keys()) if sc_map = config().aliases()
sc_map.update(_SUBCOMMAND_MAP)
subcommands = [sc for sc in sorted(sc_map.keys()) if
sc.startswith(p_word_before_cursor)] sc.startswith(p_word_before_cursor)]
for command in subcommands: for command in subcommands:
yield Completion(command, -len(p_word_before_cursor)) yield Completion(command, -len(p_word_before_cursor))
......
...@@ -33,6 +33,11 @@ DEFAULT_EDITOR = 'vi' ...@@ -33,6 +33,11 @@ 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)
def _get_file_mtime(p_file):
return os.stat(p_file.name).st_mtime
def _is_edited(p_orig_mtime, p_file):
return p_orig_mtime < _get_file_mtime(p_file)
class EditCommand(MultiCommand): class EditCommand(MultiCommand):
def __init__(self, p_args, p_todolist, p_output, p_error, p_input): def __init__(self, p_args, p_todolist, p_output, p_error, p_input):
...@@ -105,10 +110,12 @@ class EditCommand(MultiCommand): ...@@ -105,10 +110,12 @@ class EditCommand(MultiCommand):
self.printer.add_filter(PrettyPrinterNumbers(self.todolist)) self.printer.add_filter(PrettyPrinterNumbers(self.todolist))
temp_todos = self._todos_to_temp() temp_todos = self._todos_to_temp()
orig_mtime = _get_file_mtime(temp_todos)
if not self._open_in_editor(temp_todos.name): if not self._open_in_editor(temp_todos.name):
new_todos = self._todos_from_temp(temp_todos) new_todos = self._todos_from_temp(temp_todos)
if len(new_todos) == len(self.todos):
if _is_edited(orig_mtime, temp_todos):
for todo in self.todos: for todo in self.todos:
BASE_TODOLIST(self.todolist).delete(todo) BASE_TODOLIST(self.todolist).delete(todo)
...@@ -116,8 +123,7 @@ class EditCommand(MultiCommand): ...@@ -116,8 +123,7 @@ class EditCommand(MultiCommand):
self.todolist.add_todo(todo) self.todolist.add_todo(todo)
self.out(self.printer.print_todo(todo)) self.out(self.printer.print_todo(todo))
else: else:
self.error('Number of edited todos is not equal to ' self.error('Editing aborted. Nothing to do.')
'number of supplied todo IDs.')
else: else:
self.error(self.usage()) self.error(self.usage())
......
...@@ -16,8 +16,6 @@ ...@@ -16,8 +16,6 @@
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib.ExpressionCommand import ExpressionCommand from topydo.lib.ExpressionCommand import ExpressionCommand
from topydo.lib.IcalPrinter import IcalPrinter
from topydo.lib.JsonPrinter import JsonPrinter
from topydo.lib.PrettyPrinter import pretty_printer_factory from topydo.lib.PrettyPrinter import pretty_printer_factory
from topydo.lib.PrettyPrinterFilter import (PrettyPrinterHideTagFilter, from topydo.lib.PrettyPrinterFilter import (PrettyPrinterHideTagFilter,
PrettyPrinterIndentFilter) PrettyPrinterIndentFilter)
...@@ -58,9 +56,11 @@ class ListCommand(ExpressionCommand): ...@@ -58,9 +56,11 @@ class ListCommand(ExpressionCommand):
self.sort_expression = value self.sort_expression = value
elif opt == '-f': elif opt == '-f':
if value == 'json': if value == 'json':
from topydo.lib.JsonPrinter import JsonPrinter
self.printer = JsonPrinter() self.printer = JsonPrinter()
elif value == 'ical': elif value == 'ical':
if self._poke_icalendar(): if self._poke_icalendar():
from topydo.lib.IcalPrinter import IcalPrinter
self.printer = IcalPrinter(self.todolist) self.printer = IcalPrinter(self.todolist)
else: else:
self.printer = None self.printer = None
......
...@@ -39,8 +39,7 @@ class SortCommand(Command): ...@@ -39,8 +39,7 @@ class SortCommand(Command):
sorter = Sorter(expression) # TODO: validate sorter = Sorter(expression) # TODO: validate
sorted_todos = sorter.sort(self.todolist.todos()) sorted_todos = sorter.sort(self.todolist.todos())
self.todolist.erase() self.todolist.replace(sorted_todos)
self.todolist.add_todos(sorted_todos)
def usage(self): def usage(self):
return """Synopsis: sort [expression]""" return """Synopsis: sort [expression]"""
......
...@@ -110,7 +110,7 @@ class ChangeSet(object): ...@@ -110,7 +110,7 @@ class ChangeSet(object):
self._write() self._write()
self.close() self.close()
def delete(self, p_timestamp=None): def delete(self, p_timestamp=None, p_write=True):
""" Removes backup from the backup file. """ """ Removes backup from the backup file. """
timestamp = p_timestamp or self.timestamp timestamp = p_timestamp or self.timestamp
index = self._get_index() index = self._get_index()
...@@ -119,6 +119,8 @@ class ChangeSet(object): ...@@ -119,6 +119,8 @@ class ChangeSet(object):
del self.backup_dict[timestamp] del self.backup_dict[timestamp]
index.remove(index[[change[0] for change in index].index(timestamp)]) index.remove(index[[change[0] for change in index].index(timestamp)])
self._save_index(index) self._save_index(index)
if p_write:
self._write() self._write()
except KeyError: except KeyError:
pass pass
...@@ -143,12 +145,15 @@ class ChangeSet(object): ...@@ -143,12 +145,15 @@ class ChangeSet(object):
""" """
Removes oldest backups that exceed the limit configured in backup_count Removes oldest backups that exceed the limit configured in backup_count
option. option.
Does not write back to file system, make sure to call self._write()
afterwards.
""" """
index = self._get_index() index = self._get_index()
backup_limit = config().backup_count() - 1 backup_limit = config().backup_count() - 1
for changeset in index[backup_limit:]: for changeset in index[backup_limit:]:
self.delete(changeset[0]) self.delete(changeset[0], p_write=False)
def get_backup(self, p_todolist): def get_backup(self, p_todolist):
""" """
......
...@@ -16,9 +16,9 @@ ...@@ -16,9 +16,9 @@
import os import os
from six import iteritems
from six.moves import configparser from six.moves import configparser
class ConfigError(Exception): class ConfigError(Exception):
def __init__(self, p_text): def __init__(self, p_text):
self.text = p_text self.text = p_text
...@@ -42,6 +42,7 @@ class _Config: ...@@ -42,6 +42,7 @@ class _Config:
""" """
self.sections = [ self.sections = [
'add', 'add',
'aliases',
'colorscheme', 'colorscheme',
'dep', 'dep',
'ls', 'ls',
...@@ -51,47 +52,71 @@ class _Config: ...@@ -51,47 +52,71 @@ class _Config:
] ]
self.defaults = { self.defaults = {
# topydo 'topydo': {
'default_command': 'ls', 'default_command': 'ls',
'colors': '1', 'colors': '1',
'filename': 'todo.txt', 'filename': 'todo.txt',
'archive_filename': 'done.txt', 'archive_filename': 'done.txt',
'identifiers': 'linenumber', 'identifiers': 'linenumber',
'backup_count': '5', 'backup_count': '5',
},
# add 'add': {
'auto_creation_date': '1', 'auto_creation_date': '1',
},
# ls 'ls': {
'hide_tags': 'id,p,ical', 'hide_tags': 'id,p,ical',
'indent': 0, 'indent': '0',
'list_limit': '-1', 'list_limit': '-1',
},
# tags 'tags': {
'tag_start': 't', 'tag_start': 't',
'tag_due': 'due', 'tag_due': 'due',
'tag_star': 'star', 'tag_star': 'star',
},
# sort 'sort': {
'keep_sorted': '0', 'keep_sorted': '0',
'sort_string': 'desc:importance,due,desc:priority', 'sort_string': 'desc:importance,due,desc:priority',
'ignore_weekends': '1', 'ignore_weekends': '1',
},
# dep 'dep': {
'append_parent_projects': '0', 'append_parent_projects': '0',
'append_parent_contexts': '0', 'append_parent_contexts': '0',
},
# colorscheme 'colorscheme': {
'project_color': 'red', 'project_color': 'red',
'context_color': 'magenta', 'context_color': 'magenta',
'metadata_color': 'green', 'metadata_color': 'green',
'link_color': 'cyan', 'link_color': 'cyan',
'priority_colors': 'A:cyan,B:yellow,C:blue', 'priority_colors': 'A:cyan,B:yellow,C:blue',
},
'aliases': {
'lsproj': 'lsprj',
'listprj': 'lsprj',
'listproj': 'lsprj',
'listproject': 'lsprj',
'listprojects': 'lsprj',
'listcon': 'lscon',
'listcontext': 'lscon',
'listcontexts': 'lscon',
},
} }
self.config = {} self.config = {}
self.cp = configparser.ConfigParser(self.defaults) self.cp = configparser.ConfigParser()
for section in self.defaults:
self.cp.add_section(section)
for option, value in iteritems(self.defaults[section]):
self.cp.set(section, option, value)
files = [ files = [
"/etc/topydo.conf", "/etc/topydo.conf",
...@@ -129,7 +154,7 @@ class _Config: ...@@ -129,7 +154,7 @@ class _Config:
try: try:
return self.cp.getboolean('topydo', 'colors') return self.cp.getboolean('topydo', 'colors')
except ValueError: except ValueError:
return self.defaults['colors'] == '1' return self.defaults['topydo']['colors'] == '1'
def todotxt(self): def todotxt(self):
return os.path.expanduser(self.cp.get('topydo', 'filename')) return os.path.expanduser(self.cp.get('topydo', 'filename'))
...@@ -147,25 +172,25 @@ class _Config: ...@@ -147,25 +172,25 @@ class _Config:
value = 0 value = 0
return value return value
except ValueError: except ValueError:
return int(self.defaults['backup_count']) return int(self.defaults['topydo']['backup_count'])
def list_limit(self): def list_limit(self):
try: try:
return self.cp.getint('ls', 'list_limit') return self.cp.getint('ls', 'list_limit')
except ValueError: except ValueError:
return int(self.defaults['list_limit']) return int(self.defaults['ls']['list_limit'])
def list_indent(self): def list_indent(self):
try: try:
return self.cp.getint('ls', 'indent') return self.cp.getint('ls', 'indent')
except ValueError: except ValueError:
return int(self.defaults['indent']) return int(self.defaults['ls']['indent'])
def keep_sorted(self): def keep_sorted(self):
try: try:
return self.cp.getboolean('sort', 'keep_sorted') return self.cp.getboolean('sort', 'keep_sorted')
except ValueError: except ValueError:
return self.defaults['keep_sorted'] == '1' return self.defaults['sort']['keep_sorted'] == '1'
def sort_string(self): def sort_string(self):
return self.cp.get('sort', 'sort_string') return self.cp.get('sort', 'sort_string')
...@@ -174,19 +199,19 @@ class _Config: ...@@ -174,19 +199,19 @@ class _Config:
try: try:
return self.cp.getboolean('sort', 'ignore_weekends') return self.cp.getboolean('sort', 'ignore_weekends')
except ValueError: except ValueError:
return self.defaults['ignore_weekends'] == '1' return self.defaults['sort']['ignore_weekends'] == '1'
def append_parent_projects(self): def append_parent_projects(self):
try: try:
return self.cp.getboolean('dep', 'append_parent_projects') return self.cp.getboolean('dep', 'append_parent_projects')
except ValueError: except ValueError:
return self.defaults['append_parent_projects'] == '1' return self.defaults['dep']['append_parent_projects'] == '1'
def append_parent_contexts(self): def append_parent_contexts(self):
try: try:
return self.cp.getboolean('dep', 'append_parent_contexts') return self.cp.getboolean('dep', 'append_parent_contexts')
except ValueError: except ValueError:
return self.defaults['append_parent_contexts'] == '1' return self.defaults['dep']['append_parent_contexts'] == '1'
def _get_tag(self, p_tag): def _get_tag(self, p_tag):
try: try:
...@@ -232,7 +257,7 @@ class _Config: ...@@ -232,7 +257,7 @@ class _Config:
else: else:
pri_colors_dict = _str_to_dict(pri_colors_str) pri_colors_dict = _str_to_dict(pri_colors_str)
except ValueError: except ValueError:
pri_colors_dict = _str_to_dict(self.defaults['priority_colors']) pri_colors_dict = _str_to_dict(self.defaults['colorscheme']['priority_colors'])
return pri_colors_dict return pri_colors_dict
...@@ -240,31 +265,47 @@ class _Config: ...@@ -240,31 +265,47 @@ class _Config:
try: try:
return self.cp.get('colorscheme', 'project_color') return self.cp.get('colorscheme', 'project_color')
except ValueError: except ValueError:
return int(self.defaults['project_color']) return int(self.defaults['colorscheme']['project_color'])
def context_color(self): def context_color(self):
try: try:
return self.cp.get('colorscheme', 'context_color') return self.cp.get('colorscheme', 'context_color')
except ValueError: except ValueError:
return int(self.defaults['context_color']) return int(self.defaults['colorscheme']['context_color'])
def metadata_color(self): def metadata_color(self):
try: try:
return self.cp.get('colorscheme', 'metadata_color') return self.cp.get('colorscheme', 'metadata_color')
except ValueError: except ValueError:
return int(self.defaults['metadata_color']) return int(self.defaults['colorscheme']['metadata_color'])
def link_color(self): def link_color(self):
try: try:
return self.cp.get('colorscheme', 'link_color') return self.cp.get('colorscheme', 'link_color')
except ValueError: except ValueError:
return int(self.defaults['link_color']) return int(self.defaults['colorscheme']['link_color'])
def auto_creation_date(self): def auto_creation_date(self):
try: try:
return self.cp.getboolean('add', 'auto_creation_date') return self.cp.getboolean('add', 'auto_creation_date')
except ValueError: except ValueError:
return self.defaults['auto_creation_date'] == '1' return self.defaults['add']['auto_creation_date'] == '1'
def aliases(self):
"""
Returns dict with aliases names as keys and pairs of actual
subcommand and alias args as values.
"""
aliases = self.cp.items('aliases')
alias_dict = dict()
for alias, meaning in aliases:
meaning = meaning.split()
real_subcommand = meaning[0]
alias_args = meaning[1:]
alias_dict[alias] = (real_subcommand, alias_args)
return alias_dict
def config(p_path=None, p_overrides=None): def config(p_path=None, p_overrides=None):
""" """
......
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