Commit 1a75b94f authored by Bram Schoenmakers's avatar Bram Schoenmakers

Introduce `ls -f <format>` to specify the output format.

Instead of creating a subcommand for every possible output format we're
going to think of in the future, just let ListCommand handle the output
format by connecting to the right format printer.

In this commit, the 'ical' subcommand is deprecated, to be removed after
the next release.

The 'json' subcommand has been removed altogether, it existed for only a
few hours.
parent 847b2132
......@@ -10,8 +10,12 @@
read from standard input) (Jacek Sowiński - @mruwek)
* Customizable colors + additional highlighting of tags and URLs (Jacek
Sowiński (@mruwek) and @kidpixo).
* `json` subcommand introduced which dumps the todo items in JSON format.
* Make sure that the `edit` subcommand always uses the correct todo.txt file.
* `ls` subcommand has the `-f` flag to specify the output format. Currently,
three formats are supported:
* `text` - The default plain text format.
* `ical` - iCalendar (WARNING: this deprecates the `ical` subcommand)
* `json` - Javascript Object Notation (JSON)
* Resolve `~` to home directory if used in a configuration file
(@robertvanbregt).
* Various minor fixes.
......
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import codecs
import re
import sys
import unittest
from topydo.lib.Config import config
from topydo.commands.IcalCommand import IcalCommand
from test.CommandTest import CommandTest, utf8
from test.TestFacilities import load_file_to_todolist
IS_PYTHON_32 = (sys.version_info.major, sys.version_info.minor) == (3, 2)
def replace_ical_tags(p_text):
# replace identifiers with dots, since they're random.
result = re.sub(r'\bical:....\b', 'ical:....', p_text)
result = re.sub(r'\bUID:....\b', 'UID:....', result)
return result
class IcalCommandTest(CommandTest):
def setUp(self):
super(IcalCommandTest, self).setUp()
self.todolist = load_file_to_todolist("test/data/ListCommandTest.txt")
@unittest.skipIf(IS_PYTHON_32, "icalendar is not supported for Python 3.2")
def test_ical(self):
command = IcalCommand([""], self.todolist, self.out, self.error)
command.execute()
self.assertTrue(self.todolist.is_dirty())
icaltext = ""
with codecs.open('test/data/ListCommandTest.ics', 'r', encoding='utf-8') as ical:
icaltext = ical.read()
self.assertEqual(replace_ical_tags(self.output), replace_ical_tags(icaltext))
self.assertEqual(self.errors, "")
@unittest.skipUnless(IS_PYTHON_32, "icalendar is not supported for Python 3.2")
def test_ical_python32(self):
"""
Test case for Python 3.2 where icalendar is not supported.
"""
command = IcalCommand([""], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.output, '')
self.assertEqual(self.errors, "icalendar is not supported in this Python version.\n")
@unittest.skipIf(IS_PYTHON_32, "icalendar is not supported for Python 3.2")
def test_help(self):
command = IcalCommand(["help"], self.todolist, self.out, self.error)
command.execute()
self.assertEqual(self.output, "")
self.assertEqual(self.errors, command.usage() + "\n\n" + command.help() + "\n")
@unittest.skipUnless(IS_PYTHON_32, "icalendar is not supported for Python 3.2")
def test_help_python32(self):
command = IcalCommand(["help"], self.todolist, self.out, self.error)
command.execute()
self.assertEqual(self.output, "")
self.assertEqual(self.errors, "icalendar is not supported in this Python version.\n")
class IcalCommandUnicodeTest(CommandTest):
def setUp(self):
super(IcalCommandUnicodeTest, self).setUp()
self.todolist = load_file_to_todolist("test/data/ListCommandUnicodeTest.txt")
@unittest.skipIf(IS_PYTHON_32, "icalendar is not supported for Python 3.2")
def test_ical_unicode(self):
command = IcalCommand([""], self.todolist, self.out, self.error)
command.execute()
self.assertTrue(self.todolist.is_dirty())
icaltext = ""
with codecs.open('test/data/ListCommandUnicodeTest.ics', 'r', encoding='utf-8') as ical:
icaltext = ical.read()
self.assertEqual(replace_ical_tags(self.output), utf8(replace_ical_tags(icaltext)))
self.assertEqual(self.errors, "")
if __name__ == '__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/>.
import codecs
import unittest
from topydo.commands.JsonCommand import JsonCommand
from test.CommandTest import CommandTest, utf8
from test.TestFacilities import load_file_to_todolist
class JsonCommandTest(CommandTest):
def setUp(self):
super(JsonCommandTest, self).setUp()
self.todolist = load_file_to_todolist("test/data/ListCommandTest.txt")
def test_json(self):
command = JsonCommand([""], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
jsontext = ""
with codecs.open('test/data/ListCommandTest.json', 'r', encoding='utf-8') as json:
jsontext = json.read()
self.assertEqual(self.output, jsontext)
self.assertEqual(self.errors, "")
def test_help(self):
command = JsonCommand(["help"], self.todolist, self.out, self.error)
command.execute()
self.assertEqual(self.output, "")
self.assertEqual(self.errors, command.usage() + "\n\n" + command.help() + "\n")
class JsonCommandUnicodeTest(CommandTest):
def setUp(self):
super(JsonCommandUnicodeTest, self).setUp()
self.todolist = load_file_to_todolist("test/data/ListCommandUnicodeTest.txt")
def test_json_unicode(self):
command = JsonCommand([""], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
jsontext = ""
with codecs.open('test/data/ListCommandUnicodeTest.json', 'r', encoding='utf-8') as json:
jsontext = json.read()
self.assertEqual(self.output, utf8(jsontext))
self.assertEqual(self.errors, "")
if __name__ == '__main__':
unittest.main()
......@@ -15,6 +15,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from six import u
import codecs
import re
import sys
import unittest
from topydo.lib.Config import config
......@@ -181,6 +184,22 @@ class ListCommandTest(CommandTest):
self.assertEqual(self.output, "| 3| (C) Baz @Context1 +Project1 key:value id:1\n| 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n")
self.assertEqual(self.errors, "")
def test_list20(self):
command = ListCommand(["-f text"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.output, "| 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n")
self.assertEqual(self.errors, "")
def test_list21(self):
command = ListCommand(["-f invalid"], self.todolist, self.out, self.error)
command.execute()
self.assertFalse(self.todolist.is_dirty())
self.assertEqual(self.output, "| 1| (C) Foo @Context2 Not@Context +Project1 Not+Project\n| 4| (C) Drink beer @ home\n| 5| (C) 13 + 29 = 42\n| 2| (D) Bar @Context1 +Project2\n")
self.assertEqual(self.errors, "")
def test_help(self):
command = ListCommand(["help"], self.todolist, self.out, self.error)
command.execute()
......@@ -204,5 +223,93 @@ class ListCommandUnicodeTest(CommandTest):
self.assertEqual(self.output, expected)
class ListCommandJsonTest(CommandTest):
def test_json(self):
todolist = load_file_to_todolist("test/data/ListCommandTest.txt")
command = ListCommand(["-f", "json"], todolist, self.out, self.error)
command.execute()
self.assertFalse(todolist.is_dirty())
jsontext = ""
with codecs.open('test/data/ListCommandTest.json', 'r', encoding='utf-8') as json:
jsontext = json.read()
self.assertEqual(self.output, jsontext)
self.assertEqual(self.errors, "")
def test_json_unicode(self):
todolist = load_file_to_todolist("test/data/ListCommandUnicodeTest.txt")
command = ListCommand(["-f", "json"], todolist, self.out, self.error)
command.execute()
self.assertFalse(todolist.is_dirty())
jsontext = ""
with codecs.open('test/data/ListCommandUnicodeTest.json', 'r', encoding='utf-8') as json:
jsontext = json.read()
self.assertEqual(self.output, utf8(jsontext))
self.assertEqual(self.errors, "")
def replace_ical_tags(p_text):
# replace identifiers with dots, since they're random.
result = re.sub(r'\bical:....\b', 'ical:....', p_text)
result = re.sub(r'\bUID:....\b', 'UID:....', result)
return result
IS_PYTHON_32 = (sys.version_info.major, sys.version_info.minor) == (3, 2)
class ListCommandIcalTest(CommandTest):
@unittest.skipIf(IS_PYTHON_32, "icalendar is not supported for Python 3.2")
def test_ical(self):
todolist = load_file_to_todolist("test/data/ListCommandTest.txt")
command = ListCommand(["-f", "ical"], todolist, self.out, self.error)
command.execute()
self.assertTrue(todolist.is_dirty())
icaltext = ""
with codecs.open('test/data/ListCommandTest.ics', 'r', encoding='utf-8') as ical:
icaltext = ical.read()
self.assertEqual(replace_ical_tags(self.output), replace_ical_tags(icaltext))
self.assertEqual(self.errors, "")
@unittest.skipUnless(IS_PYTHON_32, "icalendar is not supported for Python 3.2")
def test_ical_python32(self):
"""
Test case for Python 3.2 where icalendar is not supported.
"""
todolist = load_file_to_todolist("test/data/ListCommandTest.txt")
command = ListCommand(["-f", "ical"], todolist, self.out, self.error)
command.execute()
self.assertFalse(todolist.is_dirty())
self.assertEqual(self.output, '')
self.assertEqual(self.errors, "icalendar is not supported in this Python version.\n")
@unittest.skipIf(IS_PYTHON_32, "icalendar is not supported for Python 3.2")
def test_ical_unicode(self):
todolist = load_file_to_todolist("test/data/ListCommandUnicodeTest.txt")
command = ListCommand(["-f", "ical"], todolist, self.out, self.error)
command.execute()
self.assertTrue(todolist.is_dirty())
icaltext = ""
with codecs.open('test/data/ListCommandUnicodeTest.ics', 'r', encoding='utf-8') as ical:
icaltext = ical.read()
self.assertEqual(replace_ical_tags(self.output), utf8(replace_ical_tags(icaltext)))
self.assertEqual(self.errors, "")
if __name__ == '__main__':
unittest.main()
......@@ -33,8 +33,7 @@ _SUBCOMMAND_MAP = {
'do': 'DoCommand',
'edit': 'EditCommand',
'exit': 'ExitCommand', # used for the prompt
'ical': 'IcalCommand',
'json': 'JsonCommand',
'ical': 'IcalCommand', # deprecated
'ls': 'ListCommand',
'lscon': 'ListContextCommand',
'listcon': 'ListContextCommand',
......
......@@ -15,13 +15,14 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Implements a subcommand that outputs an iCalendar file.
Stub for the former 'ical' subcommand, now replaced with 'ls -f ical'.
To be removed.
"""
from topydo.lib.IcalPrinter import IcalPrinter
from topydo.commands.ListCommand import ListCommand
from topydo.lib.Command import Command
class IcalCommand(ListCommand):
class IcalCommand(Command):
def __init__(self, p_args, p_todolist,
p_out=lambda a: None,
p_err=lambda a: None,
......@@ -29,47 +30,14 @@ class IcalCommand(ListCommand):
super(IcalCommand, self).__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
self.printer = IcalPrinter(p_todolist)
def _print(self):
self.out(str(self._view()))
def execute(self):
try:
import icalendar as _
except ImportError:
self.error("icalendar package is not installed.")
return False
except SyntaxError:
self.error("icalendar is not supported in this Python version.")
return False
return super(IcalCommand, self).execute()
self.error("The 'ical' subcommand is deprecated, please use 'ls -f ical' instead.")
return False
def usage(self):
return """Synopsis: ical [-x] [expression]"""
return """Synopsis: ical"""
def help(self):
return """\
Similar to the 'ls' subcommand, except that the todos are printed in iCalendar
format (RFC 2445) that can be imported by other calendar applications.
By default prints the active todo items, possibly filtered by the given
expression.
For the supported options, please refer to the help text of 'ls'
(topydo help ls).
While specifying the sort order is supported (-s flag), like in 'ls', this is
not meaningful in the context of an iCalendar file.
Note 1 : be aware that this is not necessarily a read-only operation. This
subcommand may add ical tags to the printed todo items containing a unique ID.
Completed todo items may be archived.
Note 2: topydo does not support reading iCal files, this is merely a dump.
Changes made with other iCalendar enabled applications will not be processed.
Suggested usage is to use the output as a read-only calendar.
Note 3: The ical subcommand only works for Python 2.7 and 3.3+.
Deprecated. Use 'ls -f ical' instead.
"""
# 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/>.
"""
Implements a subcommand that outputs an JSON file.
"""
from topydo.lib.JsonPrinter import JsonPrinter
from topydo.commands.ListCommand import ListCommand
class JsonCommand(ListCommand):
def __init__(self, p_args, p_todolist,
p_out=lambda a: None,
p_err=lambda a: None,
p_prompt=lambda a: None):
super(JsonCommand, self).__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
self.printer = JsonPrinter()
def _print(self):
self.out(str(self._view()))
def execute(self):
return super(JsonCommand, self).execute()
def usage(self):
return """Synopsis: json [-x] [expression]"""
def help(self):
return """\
Similar to the 'ls' subcommand, except that the todos are printed in JSON
format such that other applications can process it.
By default prints the active todo items, possibly filtered by the given
expression.
For the supported options, please refer to the help text of 'ls'
(topydo help ls).
Note: topydo does not support reading JSON files, this is merely a dump.
"""
......@@ -14,17 +14,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
from topydo.lib.ExpressionCommand import ExpressionCommand
from topydo.lib.Config import config
from topydo.lib import Filter
from topydo.lib.PrettyPrinterFilter import (
PrettyPrinterIndentFilter,
PrettyPrinterHideTagFilter
)
from topydo.lib.Sorter import Sorter
from topydo.lib.View import View
from topydo.lib.IcalPrinter import IcalPrinter
from topydo.lib.JsonPrinter import JsonPrinter
class ListCommand(ExpressionCommand):
def __init__(self, p_args, p_todolist,
......@@ -34,30 +31,73 @@ class ListCommand(ExpressionCommand):
super(ListCommand, self).__init__(
p_args, p_todolist, p_out, p_err, p_prompt)
self.printer = None
self.sort_expression = config().sort_string()
self.show_all = False
def _poke_icalendar(self):
"""
Attempts to import the icalendar package. Returns True if it
succeeds, otherwise False
"""
try:
import icalendar as _
except ImportError:
self.error("icalendar package is not installed.")
return False
except SyntaxError:
self.error("icalendar is not supported in this Python version.")
return False
return True
def _process_flags(self):
opts, args = self.getopt('s:x')
opts, args = self.getopt('f:s:x')
for opt, value in opts:
if opt == '-x':
self.show_all = True
elif opt == '-s':
self.sort_expression = value
elif opt == '-f':
if value == 'json':
self.printer = JsonPrinter()
elif value == 'ical':
if self._poke_icalendar():
self.printer = IcalPrinter(self.todolist)
else:
self.printer = None
self.args = args
def _print(self):
""" Prints the todos. """
indent = config().list_indent()
hidden_tags = config().hidden_tags()
filters = []
filters.append(PrettyPrinterIndentFilter(indent))
filters.append(PrettyPrinterHideTagFilter(hidden_tags))
self.out(self._view().pretty_print(filters))
"""
Prints the todos in the right format.
Defaults to normal text output (with possible colors and other pretty
printing. If a format was specified on the commandline, this format is
sent to the output.
"""
def _print_text():
"""
Outputs a pretty-printed text format of the todo list.
"""
indent = config().list_indent()
hidden_tags = config().hidden_tags()
filters = []
filters.append(PrettyPrinterIndentFilter(indent))
filters.append(PrettyPrinterHideTagFilter(hidden_tags))
self.out(self._view().pretty_print(filters))
if self.printer == None:
_print_text()
else:
# we have set a special format, simply use the printer set in
# self.printer
self.out(str(self._view()))
def execute(self):
if not super(ListCommand, self).execute():
......@@ -69,7 +109,7 @@ class ListCommand(ExpressionCommand):
return True
def usage(self):
return """Synopsis: ls [-x] [-s <sort_expression>] [expression]"""
return """ Synopsis: ls [-x] [-s <sort_expression>] [-f <format>] [expression]"""
def help(self):
return """\
......@@ -81,6 +121,14 @@ Lists all relevant todos. A todo is relevant when:
When an expression is given, only the todos matching that expression are shown.
-f : Specify the output format, being 'text' (default), 'ical' or 'json'.
* 'text' - Text output with colors and identation if applicable.
* 'ical' - iCalendar (RFC 2445). Is not supported in Python 3.2. Be aware
that this is not a read-only operation, todo items may obtain
an 'ical' tag with a unique ID. Completed todo items may be
archived.
* 'json' - Javascript Object Notation (JSON)
-s : Sort the list according to a sort expression. Defaults to the expression
in the configuration.
-x : Show all todos (i.e. do not filter on dependencies or relevance).
......
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