Commit 7b381d2b authored by Bram Schoenmakers's avatar Bram Schoenmakers

Introduce configuration files.

The following configuration files are read:

/etc/topydo.conf
$HOME/.topydo
.topydo
topydo.conf

Values in a file lower in this list prevail over values defined earlier.
parent a15654e0
[topydo]
default_action = ls
colors = 1
highlight_projects_contexts = 1
list_limit = 25
[paths]
; filename = todo.txt
; archive_filename = done.txt
[tags]
tag_start = t
tag_due = due
tag_star = star
[sort]
sort_string = desc:importance,due,desc:priority
; For calculating importance
ignore_weekends = 1
......@@ -17,7 +17,7 @@
from datetime import date
import unittest
import Config
from Config import config
from Importance import importance
import Todo
......@@ -31,10 +31,10 @@ class ImportanceTest(unittest.TestCase):
self.assertEqual(importance(todo), 5)
def test_importance3(self):
todo = Todo.Todo("(A) Foo " + Config.TAG_STAR + ":1")
todo = Todo.Todo("(A) Foo " + config().tag_star() + ":1")
self.assertEqual(importance(todo), 6)
def test_importance4(self):
today_str = date.today().isoformat()
todo = Todo.Todo("(C) Foo " + Config.TAG_DUE + ":" + today_str)
todo = Todo.Todo("(C) Foo " + config().tag_due() + ":" + today_str)
self.assertEqual(importance(todo), 8)
......@@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import Config
from Config import config, _Config
import CommandTest
import ListCommand
import TestFacilities
......@@ -96,8 +96,7 @@ class ListCommandTest(CommandTest.CommandTest):
self.assertEquals(self.errors, "")
def test_list11(self):
old_limit = Config.LIST_LIMIT
Config.LIST_LIMIT = 1
config("data/listcommand.conf")
command = ListCommand.ListCommand(["project"], self.todolist, self.out, self.errors)
command.execute()
......@@ -106,11 +105,11 @@ class ListCommandTest(CommandTest.CommandTest):
self.assertEquals(self.output, " 1 (C) Foo @Context2 Not@Context +Project1 Not+Project\n")
self.assertEquals(self.errors, "")
Config.LIST_LIMIT = old_limit
# restore
config("")
def test_list12(self):
old_limit = Config.LIST_LIMIT
Config.LIST_LIMIT = 1
config("data/listcommand.conf")
command = ListCommand.ListCommand(["-x", "project"], self.todolist, self.out, self.errors)
command.execute()
......@@ -119,7 +118,7 @@ class ListCommandTest(CommandTest.CommandTest):
self.assertEquals(self.output, " 1 (C) Foo @Context2 Not@Context +Project1 Not+Project\n 3 (C) Baz @Context1 +Project1 key:value id:1\n 2 (D) Bar @Context1 +Project2 p:1\n")
self.assertEquals(self.errors, "")
Config.LIST_LIMIT = old_limit
config("")
def test_help(self):
command = ListCommand.ListCommand(["help"], self.todolist, self.out, self.error)
......
......@@ -17,7 +17,7 @@
from datetime import date, timedelta
import unittest
import Config
from Config import config
from Recurrence import advance_recurring_todo, NoRecurrenceException
import Todo
......@@ -30,7 +30,7 @@ class RelativeDateTester(unittest.TestCase):
future = date.today() + timedelta(1)
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 = advance_recurring_todo(self.todo)
self.assertEquals(new_todo.due_date(), new_due)
......@@ -40,7 +40,7 @@ class RelativeDateTester(unittest.TestCase):
today = date.today()
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 = advance_recurring_todo(self.todo)
self.assertEquals(new_todo.due_date(), new_due)
......@@ -50,7 +50,7 @@ class RelativeDateTester(unittest.TestCase):
past = date.today() - timedelta(8)
new_due = date.today() + timedelta(7)
self.todo.set_tag(Config.TAG_DUE, past.isoformat())
self.todo.set_tag(config().tag_due(), past.isoformat())
new_todo = advance_recurring_todo(self.todo)
self.assertEquals(new_todo.due_date(), new_due)
......@@ -59,14 +59,14 @@ class RelativeDateTester(unittest.TestCase):
new_due = date.today() + timedelta(7)
new_todo = advance_recurring_todo(self.todo)
self.assertTrue(new_todo.has_tag(Config.TAG_DUE))
self.assertTrue(new_todo.has_tag(config().tag_due()))
self.assertEquals(new_todo.due_date(), new_due)
def test_startdate(self):
""" Start date is before due date. """
self.todo.set_tag(Config.TAG_DUE, date.today().isoformat())
self.todo.set_tag(config().tag_due(), date.today().isoformat())
yesterday = date.today() - timedelta(1)
self.todo.set_tag(Config.TAG_START, yesterday.isoformat())
self.todo.set_tag(config().tag_start(), yesterday.isoformat())
new_start = date.today() + timedelta(6)
new_todo = advance_recurring_todo(self.todo)
......@@ -75,8 +75,8 @@ class RelativeDateTester(unittest.TestCase):
def test_startdate2(self):
""" Start date equals due date. """
self.todo.set_tag(Config.TAG_DUE, date.today().isoformat())
self.todo.set_tag(Config.TAG_START, date.today().isoformat())
self.todo.set_tag(config().tag_due(), date.today().isoformat())
self.todo.set_tag(config().tag_start(), date.today().isoformat())
new_start = date.today() + timedelta(7)
new_todo = advance_recurring_todo(self.todo)
......
......@@ -15,7 +15,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import CommandTest
import Config
import SortCommand
import TestFacilities
......
......@@ -16,7 +16,7 @@
import unittest
import Config
from Config import config
import Sorter
import TodoList
import View
......@@ -66,7 +66,7 @@ class SorterTest(unittest.TestCase):
def test_sort4(self):
""" Ascendingly sorted by due date """
sorter = Sorter.Sorter(Config.TAG_DUE)
sorter = Sorter.Sorter(config().tag_due())
self.sort_file('data/SorterTest4.txt', 'data/SorterTest4-result.txt', sorter)
def test_sort5(self):
......
[topydo]
list_limit = 1
......@@ -18,26 +18,6 @@
import sys
from topydo.lib.AddCommand import AddCommand
from topydo.lib.AppendCommand import AppendCommand
from topydo.lib.ArchiveCommand import ArchiveCommand
from topydo.lib.DeleteCommand import DeleteCommand
from topydo.lib.DepCommand import DepCommand
from topydo.lib.DepriCommand import DepriCommand
from topydo.lib import Config
from topydo.lib.DoCommand import DoCommand
from topydo.lib.ListCommand import ListCommand
from topydo.lib.ListContextCommand import ListContextCommand
from topydo.lib.ListProjectCommand import ListProjectCommand
from topydo.lib.PostponeCommand import PostponeCommand
from topydo.lib.PrettyPrinter import *
from topydo.lib.PriorityCommand import PriorityCommand
from topydo.lib.SortCommand import SortCommand
from topydo.lib.TagCommand import TagCommand
from topydo.lib import TodoFile
from topydo.lib import TodoList
from topydo.lib.Utils import escape_ansi
def usage():
""" Prints the usage of the todo.txt CLI """
exit(1)
......@@ -69,6 +49,41 @@ def write(p_file, p_string):
if p_string:
p_file.write(p_string + "\n")
def error(p_string):
""" Writes an error on the standard error. """
write(sys.stderr, p_string)
from topydo.lib.Config import config, ConfigError
# First thing is to poke the configuration and check whether it's sane
# The modules below may already read in configuration upon import, so
# make sure to bail out if the configuration is invalid.
try:
config()
except ConfigError as e:
error(str(e))
exit(1)
from topydo.lib.AddCommand import AddCommand
from topydo.lib.AppendCommand import AppendCommand
from topydo.lib.ArchiveCommand import ArchiveCommand
from topydo.lib.DeleteCommand import DeleteCommand
from topydo.lib.DepCommand import DepCommand
from topydo.lib.DepriCommand import DepriCommand
from topydo.lib.DoCommand import DoCommand
from topydo.lib.ListCommand import ListCommand
from topydo.lib.ListContextCommand import ListContextCommand
from topydo.lib.ListProjectCommand import ListProjectCommand
from topydo.lib.PostponeCommand import PostponeCommand
from topydo.lib.PrettyPrinter import *
from topydo.lib.PriorityCommand import PriorityCommand
from topydo.lib.SortCommand import SortCommand
from topydo.lib.TagCommand import TagCommand
from topydo.lib import TodoFile
from topydo.lib import TodoList
from topydo.lib.Utils import escape_ansi
class CLIApplication(object):
def __init__(self):
self.todolist = TodoList.TodoList([])
......@@ -80,7 +95,7 @@ class CLIApplication(object):
This means that all completed tasks are moved to the archive file
(defaults to done.txt).
"""
archive_file = TodoFile.TodoFile(Config.ARCHIVE_FILENAME)
archive_file = TodoFile.TodoFile(config().archive())
archive = TodoList.TodoList(archive_file.read())
if archive:
......@@ -92,13 +107,13 @@ class CLIApplication(object):
def run(self):
""" Main entry function. """
todofile = TodoFile.TodoFile(Config.FILENAME)
todofile = TodoFile.TodoFile(config().todotxt())
self.todolist = TodoList.TodoList(todofile.read())
try:
subcommand = sys.argv[1]
except IndexError:
subcommand = Config.DEFAULT_ACTION
subcommand = config().default_action()
subcommand_map = {
'add': AddCommand,
......@@ -126,12 +141,12 @@ class CLIApplication(object):
args = arguments()
if not subcommand in subcommand_map:
subcommand = Config.DEFAULT_ACTION
subcommand = config().default_action()
args = arguments(1)
command = subcommand_map[subcommand](args, self.todolist,
lambda o: write(sys.stdout, o),
lambda e: write(sys.stderr, e),
error,
raw_input)
if command.execute() == False:
......
......@@ -19,7 +19,7 @@
from datetime import date
import re
import Config
from Config import config
import Command
from PrettyPrinter import pretty_print
from RelativeDate import relative_date_to_date
......@@ -75,8 +75,8 @@ class AddCommand(Command.Command):
self.todo.remove_tag(p_tag, raw_value)
convert_date(Config.TAG_START)
convert_date(Config.TAG_DUE)
convert_date(config().tag_start())
convert_date(config().tag_due())
add_dependencies('partof')
add_dependencies('before')
......
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 Bram Schoenmakers <me@bramschoenmakers.nl>
#
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
This module contains some definitions to configure the application.
"""
import os
DEFAULT_ACTION = 'ls'
COLORS = True
HIGHLIGHT_PROJECTS_CONTEXTS = True
LIST_LIMIT=25
import ConfigParser
FILENAME = 'todo.txt'
ARCHIVE_FILENAME = 'done.txt'
class ConfigError(Exception):
def __init__(self, p_text):
self.text = p_text
TAG_START = 't'
TAG_DUE = 'due'
TAG_STAR = 'star'
def __str__(self):
return self.text
SORT_STRING = 'desc:importance,due,desc:priority'
IGNORE_WEEKENDS = True # for calculating the importance value
class _Config:
def __init__(self, p_path=None):
self.sections = ['topydo', 'tags', 'sort', 'paths']
self.defaults = {
# topydo
'default_action': 'ls',
'colors': '1',
'highlight_projects_contexts': '1',
'list_limit': '25',
# paths
'filename' : 'todo.txt',
'archive_filename' : 'done.txt',
# tags
'tag_start': 't',
'tag_due': 'due',
'tag_star': 'star',
# sort
'sort_string': 'desc:importance,due,desc:priority',
'ignore_weekends': '1',
}
self.cp = ConfigParser.SafeConfigParser(self.defaults)
files = ["/etc/topydo.conf", self._home_config_path(), ".topydo", "topydo.conf"]
if p_path != None:
files.append(p_path)
self.cp.read(files)
self._supplement_sections()
def _supplement_sections(self):
for section in self.sections:
if not self.cp.has_section(section):
self.cp.add_section(section)
def _home_config_path(self):
return os.path.join(os.getenv('HOME'), '.topydo')
def default_action(self):
return self.cp.get('topydo', 'default_action')
def colors(self):
try:
return self.cp.getboolean('topydo', 'colors')
except ValueError:
return self.defaults['colors'] == '1'
def highlight_projects_contexts(self):
try:
return self.cp.getboolean('topydo', 'highlight_projects_contexts')
except ValueError:
return self.defaults['highlight_projects_contexts'] == '1'
def todotxt(self):
return self.cp.get('paths', 'todotxt')
def archive(self):
return self.cp.get('paths', 'archive')
def list_limit(self):
try:
return self.cp.getint('topydo', 'list_limit')
except ValueError:
return int(self.defaults['list_limit'])
def sort_string(self):
return self.cp.get('sort', 'sort_string')
def ignore_weekends(self):
try:
return self.cp.getboolean('sort', 'ignore_weekends')
except ValueError:
return self.defaults['ignore_weekends'] == '1'
def tag_due(self):
return self.cp.get('tags', 'tag_due')
def tag_start(self):
return self.cp.get('tags', 'tag_start')
def tag_star(self):
return self.cp.get('tags', 'tag_star')
def config(p_path=None):
"""
Retrieve the config instance.
If a path is given, the instance is overwritten by the one that supplies an
additional filename (for testability).
"""
if not config.instance or p_path != None:
try:
config.instance = _Config(p_path)
except ConfigParser.ParsingError as e:
raise ConfigError(str(e))
return config.instance
config.instance = None
......@@ -15,7 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from Command import *
import Config
from Config import config
import Filter
import Sorter
import TodoList
......@@ -95,7 +95,7 @@ class DepCommand(Command):
self.error(self.usage())
if todos:
sorter = Sorter.Sorter(Config.SORT_STRING)
sorter = Sorter.Sorter(config().sort_string())
instance_filter = Filter.InstanceFilter(todos)
view = View.View(sorter, [instance_filter], self.todolist)
self.out(view.pretty_print())
......
......@@ -25,7 +25,7 @@ future.
from datetime import date
import Config
from Config import config
IMPORTANCE_VALUE = {'A': 3, 'B': 2, 'C': 1}
......@@ -37,7 +37,7 @@ def is_due_next_monday(p_todo):
return due and due.weekday() == 0 and today.weekday() >= 4 and \
p_todo.days_till_due()
def importance(p_todo, p_ignore_weekend=Config.IGNORE_WEEKENDS):
def importance(p_todo, p_ignore_weekend=config().ignore_weekends()):
"""
Calculates the importance of the given task.
Returns an importance of zero when the task has been completed.
......@@ -52,7 +52,7 @@ def importance(p_todo, p_ignore_weekend=Config.IGNORE_WEEKENDS):
priority = p_todo.priority()
result += IMPORTANCE_VALUE[priority] if priority in IMPORTANCE_VALUE else 0
if p_todo.has_tag(Config.TAG_DUE):
if p_todo.has_tag(config().tag_due()):
days_left = p_todo.days_till_due()
if days_left >= 7 and days_left < 14:
......@@ -69,12 +69,12 @@ def importance(p_todo, p_ignore_weekend=Config.IGNORE_WEEKENDS):
if p_ignore_weekend and is_due_next_monday(p_todo):
result += 1
if p_todo.has_tag(Config.TAG_STAR):
if p_todo.has_tag(config().tag_star()):
result += 1
return result if not p_todo.is_completed() else 0
def average_importance(p_todo, p_ignore_weekend=Config.IGNORE_WEEKENDS):
def average_importance(p_todo, p_ignore_weekend=config().ignore_weekends()):
own_importance = importance(p_todo, p_ignore_weekend)
average = 0
......
......@@ -15,7 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import Command
import Config
from Config import config
import Filter
import Sorter
......@@ -26,7 +26,7 @@ class ListCommand(Command.Command):
p_prompt=lambda a: None):
super(ListCommand, self).__init__(p_args, p_todolist, p_out, p_err, p_prompt)
self.sort_expression = Config.SORT_STRING
self.sort_expression = config().sort_string()
self.show_all = False
def _process_flags(self):
......@@ -61,7 +61,7 @@ class ListCommand(Command.Command):
grep_filters()
if not self.show_all:
filters.append(Filter.LimitFilter(Config.LIST_LIMIT))
filters.append(Filter.LimitFilter(config().list_limit()))
return filters
......
......@@ -17,6 +17,7 @@
from datetime import date, timedelta
from Command import *
from Config import config
from PrettyPrinter import *
from RelativeDate import relative_date_to_date
from TodoList import InvalidTodoException
......@@ -41,7 +42,7 @@ class PostponeCommand(Command):
self.args = args
def _get_offset(self, p_todo):
offset = p_todo.tag_value(Config.TAG_DUE, date.today().isoformat())
offset = p_todo.tag_value(config().tag_due(), date.today().isoformat())
offset_date = date_string_to_date(offset)
if offset_date < date.today():
......@@ -65,12 +66,12 @@ class PostponeCommand(Command):
new_due = relative_date_to_date(pattern, offset)
if new_due:
if self.move_start_date and todo.has_tag(Config.TAG_START):
if self.move_start_date and todo.has_tag(config().tag_start()):
length = todo.length()
new_start = new_due - timedelta(length)
todo.set_tag(Config.TAG_START, new_start.isoformat())
todo.set_tag(config().tag_start(), new_start.isoformat())
todo.set_tag(Config.TAG_DUE, new_due.isoformat())
todo.set_tag(config().tag_due(), new_due.isoformat())
self.todolist.set_dirty()
self.out(pretty_print(todo, [self.todolist.pp_number()]))
......
......@@ -18,7 +18,7 @@
import re
import Config
from Config import config
PRIORITY_COLORS = {
'A': '\033[36m', # cyan
......@@ -36,7 +36,7 @@ def pp_color(p_todo_str, p_todo):
Should be passed as a filter in the filter list of pretty_print()
"""
if Config.COLORS:
if config().colors():
color = NEUTRAL_COLOR
try:
color = PRIORITY_COLORS[p_todo.priority()]
......@@ -45,7 +45,7 @@ def pp_color(p_todo_str, p_todo):
p_todo_str = '%s%s%s' % (color, p_todo_str, NEUTRAL_COLOR)
if Config.HIGHLIGHT_PROJECTS_CONTEXTS:
if config().highlight_projects_contexts():
p_todo_str = re.sub(r'\B(\+|@)(\S*\w)', PROJECT_COLOR + r'\g<0>' + color, \
p_todo_str)
......
......@@ -18,7 +18,7 @@
from datetime import date, timedelta
import Config
from Config import config
from RelativeDate import relative_date_to_date
import Todo
......@@ -52,11 +52,11 @@ def advance_recurring_todo(p_todo):
length = todo.length()
new_due = relative_date_to_date(pattern, due)
todo.set_tag(Config.TAG_DUE, new_due.isoformat())
todo.set_tag(config().tag_due(), new_due.isoformat())
if todo.start_date():
new_start = new_due - timedelta(length)
todo.set_tag(Config.TAG_START, new_start.isoformat())
todo.set_tag(config().tag_start(), new_start.isoformat())
todo.set_creation_date(date.today())
......
......@@ -15,7 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from Command import *
import Config
from Config import config
import Filter
import Sorter
......@@ -33,7 +33,7 @@ class SortCommand(Command):
try:
expression = self.argument(0)
except InvalidCommandArgument:
expression = Config.SORT_STRING
expression = config().sort_string()
sorter = Sorter.Sorter(expression) # TODO: validate
sorted_todos = sorter.sort(self.todolist.todos())
......
......@@ -20,7 +20,7 @@ This module provides the Todo class.
from datetime import date
import Config
from Config import config
import Utils
import TodoBase
......@@ -41,11 +41,11 @@ class Todo(TodoBase.TodoBase):
def start_date(self):
""" Returns a date object of the todo's start date. """
return self.get_date(Config.TAG_START)
return self.get_date(config().tag_start())
def due_date(self):
""" Returns a date object of the todo's due date. """
return self.get_date(Config.TAG_DUE)
return self.get_date(config().tag_due())
def is_active(self):
"""
......
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