todo/todo/Todo.py

148 lines
5.0 KiB
Python

#!/bin/python3
import re
import logging
from pathlib import Path
from datetime import datetime
from typing import Optional, Type
from .TodoObject import TodoObject
from .Category import Category
from .Note import Note
from .Task import Task
logger = logging.getLogger("Todo")
logger.addHandler(logging.StreamHandler())
class Todo(TodoObject):
def __init__(self, data_file : Path = Path.home().joinpath("todo.md"), tab_spacing = 2):
"""Todo class, reads and parses a todo.md from a file. Default file is 'todo.md' in the home directory.
Args:
data_file (Path, optional): Location of the markdown file to load from. Defaults to ~/todo.md.
tab_spacing (int, optional): How many levels a tab character should represent. Defaults to 2.
"""
self.data_file = data_file
self.tab_spacing = tab_spacing
super().__init__(-1, "File")
self._load_data()
def __str__(self) -> str:
return f"File: {self.data_file.name}"
def _get_level(self, line: str) -> int:
"""Calculates the level at which this element lies by parsing the given markdown line.
Args:
line (str): The string to parse.
Returns:
int: The level, starting at 0.
"""
count = 0
if line[0] == "-" or line[0] == " ":
for char in line:
if char == " ":
count += 1
else:
break
return count // 2
elif line[0] == "#":
for char in line:
if char == "#":
count += 1
else:
break
return count - 1
else: # catch-all for unlisted notes
return 0
def _get_parent(self, level: int, obj: TodoObject, obj_type: Optional[Type[TodoObject]] = None):
"""Gets the parent for an object at level X.
If an object with a lower level cannot be found within the obj, it will return the obj.
Args:
level (int): The level to find the parent for.
obj (TodoObject): Object to grab parents from.
obj_type (Type[TodoObject], optional): The type of the object the parent should be. Default is None.
Returns:
TodoObject: The object found.
"""
if level > 0:
for child in obj.children[::-1]:
if obj_type is not None and isinstance(child, obj_type) and child.level < level:
return child
if obj_type is None and child.level < level:
return child
return obj
else:
return obj
def _parse_note(self, line: str, parent: TodoObject):
"""Parses a markdown line representing a note.
Args:
line (str): The line to parse.
category (Category): The category this note belongs to.
"""
level = self._get_level(line)
parent = self._get_parent(level, parent)
if re.match(" * -", line):
Note(level, line[level*2+2:], parent)
else:
Note(level, line[level*2:], parent, False)
def _parse_task(self, line: str, parent: Category):
"""Parses the markdown line representing a task.
Args:
line (str): The line to parse.
category (Category): The category this task belongs to.
"""
level = self._get_level(line)
parent = self._get_parent(level, parent)
if "[ ]" in line:
complete = False
elif "[x]" in line:
complete = True
line = line[level*2+6:]
if "|" in line:
line = line.replace(" |", "|")
name = line.split("|")[0]
date = datetime.strptime(line.split("|")[1], '%b %d %Y')
else:
name = line
date = None
Task(name, date, complete, level, parent)
def _parse_category(self, line: str):
"""Parses the markdown line representing a category of todos.
Args:
line (str): The line to parse.
"""
level = self._get_level(line)
parent = self._get_parent(level, self, obj_type=Category)
line = line[level+2:]
Category(line, level, parent)
def _load_data(self):
"""Load categories and tasks from self.data_file."""
with self.data_file.open("r") as f:
for line in f:
line = line.replace("\t", self.tab_spacing*" ")
line = line.rstrip()
if len(line.strip()) == 0: # skip empty lines
continue
elif line[0] == "#":
self._parse_category(line)
elif re.match(" *- \\[[x ]\\]", line):
self._parse_task(line, self.get_children(obj_type=Category)[-1])
else:
self._parse_note(line, self.get_children(obj_type=Category)[-1])
def write_data(self):
"""Write the Todo data to the datafile."""
self.data_file.write_text(self.get_md())