From ed0cb5ebc625748b9708f9829ae136754f210954 Mon Sep 17 00:00:00 2001 From: riley Date: Tue, 28 Sep 2021 00:20:20 -0400 Subject: [PATCH] Refactor for CLI General: + Improved documentation cli.py: + added basic CLI, will likely be rewritten later. TodoObject: ~ improved type-hinting for get_children + added get_parents method + moved get_md method from Todo + added get_* methods to get Tasks, Categories, and Notes from children + added property has_children Todo: ~ changed string output to "File: {self.data_file.name}" - moved get_md method to TodoObject + added write_data method Task: - removed get_* methods + added task_category method to get parent category. + added toggle_complete method. - removed __add__ method. Note: - removed get_* methods. - removed __add__ method. Category: - removed get_* methods. - removed __add__ method. --- cli.py | 78 ++++++++++++++++++++++ todo/Category.py | 14 ---- todo/Note.py | 11 ---- todo/Task.py | 18 +++-- todo/Todo.py | 42 ++---------- todo/TodoObject.py | 160 +++++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 250 insertions(+), 73 deletions(-) create mode 100755 cli.py diff --git a/cli.py b/cli.py new file mode 100755 index 0000000..c2c3426 --- /dev/null +++ b/cli.py @@ -0,0 +1,78 @@ +#!/bin/python3 +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? + +todo = Todo() + +args = parser.parse_args() + +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: + for i in data.get_tasks(): + if re.match(args.task, i.text, re.IGNORECASE): + data = i + +# --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) + 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/Category.py b/todo/Category.py index 3d65e6b..b604e3b 100644 --- a/todo/Category.py +++ b/todo/Category.py @@ -5,20 +5,6 @@ from .TodoObject import TodoObject class Category(TodoObject): def __init__(self, name : str, level: int, parent = None): super().__init__(level, name, parent) - - def get_notes(self, immediate = True): - from .Note import Note - return self.get_children(immediate, Note) - - def get_subcategories(self, immediate = True): - return self.get_children(immediate, Category) - - def get_tasks(self, immediate = True): - from .Task import Task - return self.get_children(immediate, Task) 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 f47660e..91e8c8a 100644 --- a/todo/Note.py +++ b/todo/Note.py @@ -7,19 +7,8 @@ class Note(TodoObject): self.listed = listed super().__init__(level, text, parent) - def get_subnotes(self, immediate = True): - return self.get_children(immediate, Note) - - def get_tasks(self, immediate = True): - from .Task import Task - return self.get_children(immediate, Task) - def __str__(self): if self.listed: 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 e6e7d87..d89dc98 100644 --- a/todo/Task.py +++ b/todo/Task.py @@ -11,13 +11,18 @@ class Task(TodoObject): self.complete = complete super().__init__(level, name, parent) - def get_subtasks(self, immediate = True): - return self.get_children(immediate, Task) + def task_category(self) -> Category: + """Get the category this task belongs to. - def get_notes(self, immediate = True): - from .Note import Note - return self.get_children(immediate, Note) + Returns: + Category: The category this task belongs to. + """ + return self.get_parents(Category)[-1] + def toggle_complete(self): + """Toggle this task's complete value.""" + self.complete = not self.complete + def __str__(self): output = "" if self.complete: @@ -31,5 +36,4 @@ class Task(TodoObject): output += f" {self.text}" return output - def __add__(self, other): - return other + self.__str__() +from .Category import Category diff --git a/todo/Todo.py b/todo/Todo.py index feaf599..5ff1415 100644 --- a/todo/Todo.py +++ b/todo/Todo.py @@ -27,11 +27,11 @@ class Todo(TodoObject): super().__init__(-1, "File") self._load_data() - def __str__(self): - return self.get_md() + 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. + """Calculates the level at which this element lies by parsing the given markdown line. Args: line (str): The string to parse. @@ -142,36 +142,6 @@ class Todo(TodoObject): else: self._parse_note(line, self.get_children(obj_type=Category)[-1]) - 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 isinstance(i, Category): - if i.level > 0: - output += "\n"*category_spacing - 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.get_children(obj_type=Category): - if i.text == name: - return i + def write_data(self): + """Write the Todo data to the datafile.""" + self.data_file.write_text(self.get_md()) diff --git a/todo/TodoObject.py b/todo/TodoObject.py index a6fbe1d..02c27bf 100644 --- a/todo/TodoObject.py +++ b/todo/TodoObject.py @@ -1,13 +1,15 @@ from __future__ import annotations import logging +import re -from typing import Optional, Type +from typing import Optional, Type, TypeVar 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 @@ -15,9 +17,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: Type[TodoObject] = None) -> list[TodoObject]: + 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 (Optional[Type[T]], optional): Specify the types of children to return. Defaults to None. + + Returns: + list[T]: [description] + """ output = [] for child in self.children: if obj_type is not None and isinstance(child, obj_type) and immediate and child.parent is self: @@ -30,8 +41,53 @@ class TodoObject: output.append(child) return output - def set_parents(self, parent): - parent: TodoObject + 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 (Optional[Type[T]], optional): Specify the types of parents to return. Defaults to None. + + Returns: + list[T]: 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. + """ for p in parent.parents: logger.debug(f"adding parent '{p.text}' to '{self.text}'") self.parents.append(p) @@ -42,3 +98,97 @@ class TodoObject: logger.debug(f"adding child '{self.text}' to '{parent.text}'") parent.children.append(self) self.parent = parent + + def get_tasks(self, immediate = 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 = 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: [description] + """ + for task in self.get_tasks(self, immediate, complete): + if re.match(regex, task.text): + return Task + + + def get_categories(self, immediate = 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: [description] + """ + 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: [description] + """ + for note in self.get_notes(): + 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)