Compare commits

..

11 commits

Author SHA1 Message Date
9686e43ec2 Documentation and formatting fixes 2021-09-28 01:36:12 -04:00
134d71f44d Add --file argument & fixes
cli.py:
+ added --file argument to specify the todo file.
~ changed the --task handler to use get_task instead.
~ fixed bug w/ printing information on tasks w/ no due date.
TodoObject.py:
~ modified type-hinting on get_tasks.
~ modified type-hinting on get_task.
~ fixed bug with get_task returning type Task rather than found task.
~ fixed bug with get_note not listening to the immediate parameter.
2021-09-28 00:54:49 -04:00
ff0f47ae88 Update gitignore 2021-09-28 00:22:00 -04:00
ed0cb5ebc6 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.
2021-09-28 00:20:20 -04:00
ada7812e3b Parent/Child assignment & fetching refactor
- TodoObject.get_children accepts an obj_type to filter by
- Todo._get_parent accepts an obj_type to filter potential parents
- Todo._parse_category now properly assigns parent categories
- Reworked get_* methods in Task, Category, and Note to use get_children
2021-09-25 23:26:16 -04:00
e768dae978 Add \t support to parsing. 2021-09-25 20:16:08 -04:00
a696392560 Fix get_md() and parent assignments.
- now properly outputs category spacing in get_md()
  - removed task_spacing and note_spacing, may return later.
- fixed parent assignment, now iterates through the reversed children
  list.
  - this finds the closest parent.
- TodoObject now properly returns output with get_children()
2021-09-25 19:47:55 -04:00
7c32804575 Merge branch 'main' of ssh.gitdab.com:InValidFire/todo 2021-09-25 18:47:16 -04:00
cb322bd0d4 update README.md and todo.md 2021-09-25 18:46:50 -04:00
6df8c6d11c update README.md 2021-09-25 18:35:46 -04:00
6a1ac46b59 update todo.md 2021-09-25 18:34:24 -04:00
9 changed files with 285 additions and 125 deletions

1
.gitignore vendored
View file

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

View file

@ -15,25 +15,3 @@ 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
```

84
cli.py Executable file
View file

@ -0,0 +1,84 @@
#!/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,9 +8,8 @@ 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.
- [ ] tasks can be nested on other tasks or notes.
- [x] this is a complete task with a due date |Sep 30 2021
- [x] 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.
@ -32,4 +31,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

View file

@ -1,28 +1,10 @@
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__()

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import logging
from pathlib import Path
from datetime import datetime
from typing import Optional
from typing import Optional, Type
from .TodoObject import TodoObject
from .Category import Category
@ -15,21 +15,23 @@ logger = logging.getLogger("Todo")
logger.addHandler(logging.StreamHandler())
class Todo(TodoObject):
def __init__(self, data_file : Path = Path.home().joinpath("todo.md")):
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):
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.
@ -55,23 +57,27 @@ class Todo(TodoObject):
else: # catch-all for unlisted notes
return 0
def _get_parent(self, level, object: TodoObject):
"""Gets the parent of an object at level X.
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.
object (TodoObject): Object to grab parents from.
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 object.children:
if child.level < level:
for child in obj.children[::-1]:
if obj_type is not None and isinstance(child, obj_type) and child.level < level:
return child
return object
if obj_type is None and child.level < level:
return child
return obj
else:
return object
return obj
def _parse_note(self, line: str, parent: TodoObject):
"""Parses a markdown line representing a note.
@ -117,7 +123,7 @@ class Todo(TodoObject):
line (str): The line to parse.
"""
level = self._get_level(line)
parent = self._get_parent(level, self)
parent = self._get_parent(level, self, obj_type=Category)
line = line[level+2:]
Category(line, level, parent)
@ -125,54 +131,17 @@ 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.children[-1])
self._parse_task(line, self.get_children(obj_type=Category)[-1])
else:
self._parse_note(line, self.children[-1])
self._parse_note(line, self.get_children(obj_type=Category)[-1])
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()
def write_data(self):
"""Write the Todo data to the datafile."""
self.data_file.write_text(self.get_md())

View file

@ -1,12 +1,15 @@
from __future__ import annotations
import logging
import re
from typing import Optional
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
@ -14,18 +17,76 @@ 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):
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.
"""
output = []
for child in self.children:
if immediate and child.parent is self:
if obj_type is not None and isinstance(child, obj_type) and immediate and child.parent is self:
output.append(child)
elif not immediate:
elif obj_type is None and immediate and child.parent is self:
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 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 (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:
logger.debug(f"adding parent '{p.text}' to '{self.text}'")
self.parents.append(p)
@ -36,3 +97,96 @@ 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)