Compare commits

..

No commits in common. "main" and "42067eb4f5fafd11d9d6bc6d4768f243523ebeae" have entirely different histories.

9 changed files with 125 additions and 285 deletions

1
.gitignore vendored
View file

@ -1,2 +1 @@
*/__pycache__/ */__pycache__/
.vscode/

View file

@ -15,3 +15,25 @@ todo.py is my attempt to organize myself and get things straight. I disliked the
### Sub-Section ### Sub-Section
- [ ] here's another task with a due date |Oct 13 2021 - [ ] 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
```

84
cli.py
View file

@ -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. :/")

View file

@ -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. - category headers can go as many levels as necessary.
- tasks can have due-dates, and they can either be complete, or incomplete. - tasks can have due-dates, and they can either be complete, or incomplete.
- [ ] this is an incomplete task. - [ ] this is an incomplete task.
- [x] this is a complete task with a due date |Sep 30 2021 - [x] this is a complete task.
- [x] tasks can be nested on other tasks or notes. - [ ] tasks can be nested on other tasks or notes.
- [ ] or they can be on the root of the category. - [ ] or they can be on the root of the category.
- all tasks must belong under a category though. - all tasks must belong under a category though.
- so no line 1 tasks. - so no line 1 tasks.

View file

@ -1,10 +1,28 @@
from __future__ import annotations from __future__ import annotations
from .Task import Task
from .TodoObject import TodoObject from .TodoObject import TodoObject
class Category(TodoObject): class Category(TodoObject):
def __init__(self, name : str, level: int, parent = None): def __init__(self, name : str, level: int, parent = None):
super().__init__(level, name, parent) 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): def __str__(self):
return "#"*(self.level+1) + f" {self.text}" return "#"*(self.level+1) + f" {self.text}"
def __add__(self, other):
return other + self.__str__()

View file

@ -12,3 +12,7 @@ class Note(TodoObject):
return f"- {self.text}" return f"- {self.text}"
else: else:
return self.text return self.text
def __add__(self, other: str):
return other + self.__str__()

View file

@ -3,6 +3,7 @@ from __future__ import annotations
from typing import Union from typing import Union
from datetime import datetime from datetime import datetime
from .Note import Note
from .TodoObject import TodoObject from .TodoObject import TodoObject
class Task(TodoObject): class Task(TodoObject):
@ -11,17 +12,18 @@ class Task(TodoObject):
self.complete = complete self.complete = complete
super().__init__(level, name, parent) super().__init__(level, name, parent)
def task_category(self) -> Category: def get_subtasks(self) -> list[Task]:
"""Get the category this task belongs to. tasks = []
for child in self.children:
if isinstance(child, Task):
tasks.append(child)
return tasks
Returns: def get_notes(self):
Category: The category this task belongs to. notes = []
""" for child in self.children:
return self.get_parents(Category)[-1] 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): def __str__(self):
output = "" output = ""
@ -36,4 +38,5 @@ class Task(TodoObject):
output += f" {self.text}" output += f" {self.text}"
return output return output
from .Category import Category def __add__(self, other):
return other + self.__str__()

View file

@ -4,7 +4,7 @@ import logging
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
from typing import Optional, Type from typing import Optional
from .TodoObject import TodoObject from .TodoObject import TodoObject
from .Category import Category from .Category import Category
@ -15,23 +15,21 @@ logger = logging.getLogger("Todo")
logger.addHandler(logging.StreamHandler()) logger.addHandler(logging.StreamHandler())
class Todo(TodoObject): 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. """Todo class, reads and parses a todo.md from a file. Default file is 'todo.md' in the home directory.
Args: Args:
data_file (Path, optional): Location of the markdown file to load from. Defaults to ~/todo.md. 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.data_file = data_file
self.tab_spacing = tab_spacing
super().__init__(-1, "File") super().__init__(-1, "File")
self._load_data() self._load_data()
def __str__(self) -> str: def __str__(self):
return f"File: {self.data_file.name}" return self.get_md()
def _get_level(self, line: str) -> int: 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: Args:
line (str): The string to parse. line (str): The string to parse.
@ -57,27 +55,23 @@ class Todo(TodoObject):
else: # catch-all for unlisted notes else: # catch-all for unlisted notes
return 0 return 0
def _get_parent(self, level: int, obj: TodoObject, obj_type: Optional[Type[TodoObject]] = None): def _get_parent(self, level, object: TodoObject):
"""Gets the parent for an object at level X. """Gets the parent of an object at level X.
If an object with a lower level cannot be found within the obj, it will return the obj.
Args: Args:
level (int): The level to find the parent for. level (int): The level to find the parent for.
obj (TodoObject): Object to grab parents from. object (TodoObject): Object to grab parents from.
obj_type (Type[TodoObject], optional): The type of the object the parent should be. Default is None.
Returns: Returns:
TodoObject: The object found. TodoObject: The object found.
""" """
if level > 0: if level > 0:
for child in obj.children[::-1]: for child in object.children:
if obj_type is not None and isinstance(child, obj_type) and child.level < level: if child.level < level:
return child return child
if obj_type is None and child.level < level: return object
return child
return obj
else: else:
return obj return object
def _parse_note(self, line: str, parent: TodoObject): def _parse_note(self, line: str, parent: TodoObject):
"""Parses a markdown line representing a note. """Parses a markdown line representing a note.
@ -123,7 +117,7 @@ class Todo(TodoObject):
line (str): The line to parse. line (str): The line to parse.
""" """
level = self._get_level(line) level = self._get_level(line)
parent = self._get_parent(level, self, obj_type=Category) parent = self._get_parent(level, self)
line = line[level+2:] line = line[level+2:]
Category(line, level, parent) Category(line, level, parent)
@ -131,17 +125,54 @@ class Todo(TodoObject):
"""Load categories and tasks from self.data_file.""" """Load categories and tasks from self.data_file."""
with self.data_file.open("r") as f: with self.data_file.open("r") as f:
for line in f: for line in f:
line = line.replace("\t", self.tab_spacing*" ")
line = line.rstrip() line = line.rstrip()
if len(line.strip()) == 0: # skip empty lines if len(line.strip()) == 0: # skip empty lines
continue continue
elif line[0] == "#": elif line[0] == "#":
self._parse_category(line) self._parse_category(line)
elif re.match(" *- \\[[x ]\\]", 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: else:
self._parse_note(line, self.get_children(obj_type=Category)[-1]) self._parse_note(line, self.children[-1])
def write_data(self): def get_md(self, category_spacing: int = 1, task_spacing: int = 0, note_spacing: int = 0) -> str:
"""Write the Todo data to the datafile.""" """Gets the markdown text of the current data loaded into the object.
self.data_file.write_text(self.get_md())
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()

View file

@ -1,15 +1,12 @@
from __future__ import annotations
import logging import logging
import re
from typing import Optional, Type, TypeVar from typing import Optional
logger = logging.getLogger("TodoObject") logger = logging.getLogger("TodoObject")
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler()) logger.addHandler(logging.StreamHandler())
class TodoObject: class TodoObject:
"""Base TodoObject for all classes in the todo library."""
def __init__(self, level, text, parent = None): def __init__(self, level, text, parent = None):
self.text: str = text self.text: str = text
self.level: int = level self.level: int = level
@ -17,76 +14,18 @@ class TodoObject:
self.children: list[TodoObject] = [] self.children: list[TodoObject] = []
self.parent: Optional[TodoObject] = parent self.parent: Optional[TodoObject] = parent
if parent is not None: 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]: def get_children(self, immediate = False):
"""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.
"""
output = [] output = []
for child in self.children: 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) output.append(child)
elif obj_type is None and immediate and child.parent is self: elif not immediate:
output.append(child) 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]: def set_parents(self, parent):
"""Get all parents of an object. Optionally specify a class of TodoObject to return. parent: TodoObject
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.
"""
for p in parent.parents: for p in parent.parents:
logger.debug(f"adding parent '{p.text}' to '{self.text}'") logger.debug(f"adding parent '{p.text}' to '{self.text}'")
self.parents.append(p) self.parents.append(p)
@ -97,96 +36,3 @@ class TodoObject:
logger.debug(f"adding child '{self.text}' to '{parent.text}'") logger.debug(f"adding child '{self.text}' to '{parent.text}'")
parent.children.append(self) parent.children.append(self)
self.parent = parent 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)