diff --git a/.gitignore b/.gitignore index 5420d1d..c01bd8c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ */__pycache__/ -.vscode/ \ No newline at end of file diff --git a/README.md b/README.md index 1dce8e0..ba6819f 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,25 @@ todo.py is my attempt to organize myself and get things straight. I disliked the ### Sub-Section - [ ] here's another task with a due date |Oct 13 2021 ``` + +## TODO +kinda funny how a todo tool has a todo list, anyways here's what I've got to do to finish this thing: + +```md +# todo.md + +## Features +- [ ] add recurring tasks + - thinking about having the following section for spec +- [ ] add subtasks +- [ ] add notes + +## RECURRING +- some recurring task |how often it should repeat |the last time completed + - possible values for repeat time: + - daily + - weekly - mon + - biweekly - wed + - monthly - 15 + - yearly - jan - 17 +``` diff --git a/cli.py b/cli.py deleted file mode 100755 index 0e349be..0000000 --- a/cli.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/python3 -from pathlib import Path - -import argparse -import re - -from todo.TodoObject import TodoObject - -from todo import Todo, Category, Task - -parser = argparse.ArgumentParser("Todo.md CLI Application", description="Access your todos via the command line!") - -parser.add_argument("-md", "--markdown", action="store_true", help="Show markdown.") -parser.add_argument("-cat", "--category", action="store", help="Category the command should target.") -parser.add_argument("-t", "--task", action="store", help="Target a specific task in a category.") -parser.add_argument("-i", "--info", action="store_true", help="Print information about the given object.") -parser.add_argument("-x", "--complete", action="store_true", help="Toggle a task's completed status.") # x = done, right? -parser.add_argument("-f", "--file", action="store", help="Specify the file to load into a Todo.") - -args = parser.parse_args() - -if args.file is not None: - todo = Todo(Path(args.file)) -else: - todo = Todo() - -def print_incomplete_tasks(data: TodoObject): - if len(data.get_tasks(complete=False)) > 0: - print("\n--- Incomplete Task(s) ---") - for category in [data, *data.get_categories()]: # allows us to catch tasks in subcategories and the main data object. - if len(category.get_tasks(immediate=False, complete=False)) > 0: - print(f"\n{category}") - for task in category.get_tasks(immediate=True, complete=False): - print(task) - if task.has_children: - print(task.get_md()) - -data = todo # start with base todo object as input data. - -# --category handler -if args.category is not None: - args.category = f".*{args.category}" - data = data.get_category(args.category) - -# --task handler -if args.task is not None: - args.task = f".*{args.task}" - data = data.get_task(args.task) - -# --done handler -if args.complete: - if isinstance(data, Task): - data.toggle_complete() - print(f"Set complete status for '{data.text}' to '{data.complete}'.") - todo.write_data() - else: - print("-d is only applicable to tasks.") - -## --markdown handler -if args.markdown: - print(data) - print(data.get_md()) - -## --info handler -if args.info: - task_count = len(data.get_tasks()) - incomplete_count = len(data.get_tasks(complete=False)) - if isinstance(data, Todo): - print(data) - print("Categories: ", ", ".join([x.text for x in data.get_children(obj_type=Category)])) - print(f"Tasks: \n\tTotal: {task_count}\n\tIncomplete: {incomplete_count}") - print_incomplete_tasks(data) - elif isinstance(data, Category): - print("Category:", data.text) - print(f"Tasks: \n\tTotal: {task_count}\n\tIncomplete: {incomplete_count}") - print_incomplete_tasks(data) - elif isinstance(data, Task): - print("Category: ", data.task_category().text) - print("Task:", data.text) - if data.date is not None: - print("Due Date:", data.date.strftime("%b %d %Y")) - print("Complete:", data.complete) - else: - print("Don't know how we ended up here. :/") diff --git a/todo.md b/todo.md index 55e1f53..520907a 100644 --- a/todo.md +++ b/todo.md @@ -8,8 +8,9 @@ this is the to-do list for the `todo.md` project. *woah*. - category headers can go as many levels as necessary. - tasks can have due-dates, and they can either be complete, or incomplete. - [ ] this is an incomplete task. - - [x] this is a complete task with a due date |Sep 30 2021 - - [x] tasks can be nested on other tasks or notes. + - [x] this is a complete task. + - [ ] tasks can be nested on other tasks or notes. + - [ ] or they can be on the root of the category. - all tasks must belong under a category though. - so no line 1 tasks. @@ -31,4 +32,4 @@ this is the to-do list for the `todo.md` project. *woah*. - weekly - mon - biweekly - wed - monthly - 15 - - yearly - jan - 17 + - yearly - jan - 17 \ No newline at end of file diff --git a/todo/Category.py b/todo/Category.py index b604e3b..4d9f294 100644 --- a/todo/Category.py +++ b/todo/Category.py @@ -1,10 +1,28 @@ from __future__ import annotations +from .Task import Task from .TodoObject import TodoObject class Category(TodoObject): def __init__(self, name : str, level: int, parent = None): super().__init__(level, name, parent) + + def get_subcategories(self) -> list[Category]: + categories = [] + for child in self.children: + if isinstance(child, Category): + categories.append(child) + return categories + + def get_tasks(self) -> list[Task]: + tasks = [] + for child in self.children: + if isinstance(child, Task): + tasks.append(child) + return tasks def __str__(self): return "#"*(self.level+1) + f" {self.text}" + + def __add__(self, other): + return other + self.__str__() diff --git a/todo/Note.py b/todo/Note.py index 91e8c8a..d7e0b1c 100644 --- a/todo/Note.py +++ b/todo/Note.py @@ -12,3 +12,7 @@ class Note(TodoObject): return f"- {self.text}" else: return self.text + + def __add__(self, other: str): + return other + self.__str__() + diff --git a/todo/Task.py b/todo/Task.py index d89dc98..7f15c14 100644 --- a/todo/Task.py +++ b/todo/Task.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Union from datetime import datetime +from .Note import Note from .TodoObject import TodoObject class Task(TodoObject): @@ -11,18 +12,19 @@ class Task(TodoObject): self.complete = complete super().__init__(level, name, parent) - def task_category(self) -> Category: - """Get the category this task belongs to. + def get_subtasks(self) -> list[Task]: + tasks = [] + for child in self.children: + if isinstance(child, Task): + tasks.append(child) + return tasks - Returns: - Category: The category this task belongs to. - """ - return self.get_parents(Category)[-1] + def get_notes(self): + notes = [] + for child in self.children: + if isinstance(child, Note): + notes.append(child) - def toggle_complete(self): - """Toggle this task's complete value.""" - self.complete = not self.complete - def __str__(self): output = "" if self.complete: @@ -36,4 +38,5 @@ class Task(TodoObject): output += f" {self.text}" return output -from .Category import Category + def __add__(self, other): + return other + self.__str__() diff --git a/todo/Todo.py b/todo/Todo.py index 104ea5a..6f6af30 100644 --- a/todo/Todo.py +++ b/todo/Todo.py @@ -4,7 +4,7 @@ import logging from pathlib import Path from datetime import datetime -from typing import Optional, Type +from typing import Optional from .TodoObject import TodoObject from .Category import Category @@ -15,23 +15,21 @@ 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): + def __init__(self, data_file : Path = Path.home().joinpath("todo.md")): """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 __str__(self): + return self.get_md() def _get_level(self, line: str) -> int: - """Calculates the level at which this element lies by parsing the given markdown line. + """Calculates the level at which this element lies. Args: line (str): The string to parse. @@ -57,27 +55,23 @@ class Todo(TodoObject): 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. + def _get_parent(self, level, object: TodoObject): + """Gets the parent of an object at level X. 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. + object (TodoObject): Object to grab parents from. 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: + for child in object.children: + if child.level < level: return child - if obj_type is None and child.level < level: - return child - return obj + return object else: - return obj + return object def _parse_note(self, line: str, parent: TodoObject): """Parses a markdown line representing a note. @@ -123,7 +117,7 @@ class Todo(TodoObject): line (str): The line to parse. """ level = self._get_level(line) - parent = self._get_parent(level, self, obj_type=Category) + parent = self._get_parent(level, self) line = line[level+2:] Category(line, level, parent) @@ -131,17 +125,54 @@ class Todo(TodoObject): """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]) + self._parse_task(line, self.children[-1]) else: - self._parse_note(line, self.get_children(obj_type=Category)[-1]) + self._parse_note(line, self.children[-1]) - def write_data(self): - """Write the Todo data to the datafile.""" - self.data_file.write_text(self.get_md()) + def get_md(self, category_spacing: int = 1, task_spacing: int = 0, note_spacing: int = 0) -> str: + """Gets the markdown text of the current data loaded into the object. + + Args: + category_spacing (int, optional): Amount of newlines between categories. Defaults to 1. + task_spacing (int, optional): Amount of newlines between tasks. Defaults to 0. + note_spacing (int, optional): Amount of newlines between category notes. Defaults to 0. + + Returns: + str: the markdown text generated + """ + output = "" + for i in self.get_children(immediate=True): + if isinstance(i, Category): + output += f"{i}\n" + elif isinstance(i, Task): + output += f"{i.level*2*' '}{i}\n" + elif isinstance(i, Note): + output += f"{i.level*2*' '}{i}\n" + return output + + def get_category(self, name: str) -> Optional[Category]: + """Gets the requested category by name. + + Args: + name (str): The name of the category to return. + + Returns: + Optional[Category]: Returns the category if found. + """ + for i in self.children: + if isinstance(i, Category) and i.text == name: + return i + +def main(): + """Generates a markdown of my todos""" + todo = Todo() + print(todo) + +if __name__ == "__main__": + main() diff --git a/todo/TodoObject.py b/todo/TodoObject.py index 585107f..4dbab1d 100644 --- a/todo/TodoObject.py +++ b/todo/TodoObject.py @@ -1,15 +1,12 @@ -from __future__ import annotations import logging -import re -from typing import Optional, Type, TypeVar +from typing import Optional logger = logging.getLogger("TodoObject") logger.setLevel(logging.INFO) logger.addHandler(logging.StreamHandler()) class TodoObject: - """Base TodoObject for all classes in the todo library.""" def __init__(self, level, text, parent = None): self.text: str = text self.level: int = level @@ -17,76 +14,18 @@ class TodoObject: self.children: list[TodoObject] = [] self.parent: Optional[TodoObject] = parent if parent is not None: - self._set_parents(parent) + self.set_parents(parent) - def get_children(self, immediate = False, obj_type: Optional[Type[T]] = None) -> list[T]: - """Get all children of an object. Optionally specify a class of TodoObject to return. - - Args: - immediate (bool, optional): Whether it should return immediate children only. Defaults to False. - obj_type (Type[Note | Category | Task], optional): Specify the types of children to return. Defaults to None. - - Returns: - list[Note | Category | Task]: Returns all children that match the criteria. - """ + def get_children(self, immediate = False): output = [] for child in self.children: - if obj_type is not None and isinstance(child, obj_type) and immediate and child.parent is self: + if immediate and child.parent is self: output.append(child) - elif obj_type is None and immediate and child.parent is self: + elif not immediate: output.append(child) - elif obj_type is not None and isinstance(child, obj_type) and not immediate: - output.append(child) - elif obj_type is None and not immediate: - output.append(child) - return output - def get_parents(self, obj_type: Optional[Type[T]] = None) -> list[T]: - """Get all parents of an object. Optionally specify a class of TodoObject to return. - - Args: - obj_type (Type[Note | Category | Task], optional): Specify the types of parents to return. Defaults to None. - - Returns: - list[Note | Category | Task]: List of parents found. - """ - output = [] - for parent in self.parents: - if obj_type is not None and isinstance(parent, obj_type): - output.append(parent) - if obj_type is None: - output.append(parent) - return output - - def get_md(self, category_spacing: int = 1) -> str: - """Gets the markdown text of the current data loaded into the object. - - Args: - category_spacing (int, optional): Amount of newlines between categories. Defaults to 1. - - Returns: - str: the markdown text generated - """ - output = "" - for i in self.get_children(): - if len(output) > 0: - output += "\n" - if isinstance(i, Category): - if i.level > 0: - output += "\n"*category_spacing - output += f"{i}" - elif isinstance(i, Task): - output += f"{i.level*2*' '}{i}" - elif isinstance(i, Note): - output += f"{i.level*2*' '}{i}" - return output - - def _set_parents(self, parent: TodoObject): - """Set the parents of an object by supplying the first immediate parent. - - Args: - parent (TodoObject): The immediate parent of the object. - """ + def set_parents(self, parent): + parent: TodoObject for p in parent.parents: logger.debug(f"adding parent '{p.text}' to '{self.text}'") self.parents.append(p) @@ -97,96 +36,3 @@ class TodoObject: logger.debug(f"adding child '{self.text}' to '{parent.text}'") parent.children.append(self) self.parent = parent - - def get_tasks(self, immediate: bool = False, complete: Optional[bool] = None) -> list[Task]: - """Get all tasks from the children of the TodoObject. - - Args: - immediate (bool, optional): Whether it should return immediate children only. Defaults to False. - complete (Optional[bool], optional): If true, return completed tasks only, inverse if False. Defaults to None. - - Returns: - list[Task]: List of tasks found. - """ - if complete is None: - return self.get_children(immediate, Task) - else: - output = [] - for child in self.get_children(immediate, Task): - if child.complete is complete: - output.append(child) - return output - - def get_task(self, regex: str, immediate: bool = False, complete: Optional[bool] = None) -> Task: - """Get a specific task from a supplied regular expression. - - Args: - regex (str): The regular expression to match for the text of the task. - immediate (bool, optional): Whether or not it should return immediate children only. Defaults to False. - complete (Optional[bool], optional): If true, return completed tasks only, inverse if False. Defaults to None. - - Returns: - Task: Returns the task found - """ - for task in self.get_tasks(immediate, complete): - if re.match(regex, task.text): - return task - - def get_categories(self, immediate: bool = False) -> list[Category]: - """Gets all categories from children of the TodoObject. - - Returns: - list[Category]: Returns all categories found. - """ - return self.get_children(immediate, obj_type=Category) - - def get_category(self, regex: str, immediate: bool = False) -> Category: - """Get a specific task from a supplied regular expression. - - Args: - regex (str): The regular expression to match for the text of the category. - immediate (bool, optional): Whether or not it should return immediate children only. Defaults to False. - - Returns: - Category: Returns the category found. - """ - for category in self.get_categories(immediate): - if re.match(regex, category.text): - return category - - def get_notes(self, immediate: bool = False) -> list[Note]: - """Get all notes from children of the TodoObject. - - Args: - immediate (bool, optional): Whether or not it should return immeduate children only. Defaults to False. - - Returns: - Note: Returns all notes found. - """ - return self.get_children(immediate, obj_type=Note) - - def get_note(self, regex: str, immediate: bool = False) -> Note: - """Get a specific note from a supplied regular expression. - - Args: - regex (str): The regular expression to match for the text of the note. - immediate (bool, optional): Whether or not it should return immediate children only. Defaults to False. - - Returns: - Note: Returns the note found. - """ - for note in self.get_notes(immediate): - if re.match(regex, note.text): - return note - - @property - def has_children(self) -> bool: - if len(self.children) > 0: - return True - else: - return False - -from .Note import Note -from .Category import Category -from .Task import Task -T = TypeVar("T", Note, Category, Task)