From 81c866aa06dc642a655d1a967c084ca86452c27e Mon Sep 17 00:00:00 2001 From: Vighnesh Iyer Date: Thu, 14 Mar 2024 23:32:04 -0700 Subject: [PATCH 01/11] wip --- today/parser.py | 48 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/today/parser.py b/today/parser.py index 2efb05b..9f8ee49 100644 --- a/today/parser.py +++ b/today/parser.py @@ -1,10 +1,12 @@ +from abc import ABC +from dataclasses import dataclass from typing import Tuple, List, Optional -from datetime import date +from datetime import date, timedelta import re from today.task import Task, Heading -date_defn_re = re.compile(r"\[.:.") +task_attr_re = re.compile(r"\[.:.") task_re = re.compile(r"^- \[[ xX]\] ") subtask_re = re.compile(r"^[ \t]+- \[[ xX]\] ") @@ -35,17 +37,47 @@ def handle_headings_stack(headings_stack: List[str], heading_raw: str) -> List[s return headings_stack -# extract_task_attrs - call them task attributes -class RawAttribute: - prefix: str - value: str +@dataclass +class TaskAttribute(ABC): + def is_visible(self, today: date, lookahead_days: int) -> bool: + raise NotImplementedError() + + +@dataclass +class DateAttribute(TaskAttribute): + created_date: Optional[date] + due_date: Optional[date] + reminder_date: Optional[date] + finished_date: Optional[date] + + # today = 3, due_date = 5 (not visible) + # today = 5, due_date = 5 (visible) + # today = 3, due_date = 5, lookahead_days = 1 (not visible) + # today = 3, due_date = 5, lookahead_days = 2 (visible) + def is_visible(self, today: date, lookahead_days: int) -> bool: + effective_date = today + timedelta(days=lookahead_days) + if self.due_date and effective_date >= self.due_date: + return True + elif self.reminder_date and effective_date >= self.reminder_date: + return True + else: + return False + + +@dataclass +class AssignmentAttribute(TaskAttribute): + assigned_to: str + + +def extract_task_attrs(task_title: str) -> List[TaskAttribute]: + pass -def extract_date_defns(title: str) -> Tuple[List[str], str]: +def extract_task_attrs(title: str) -> List[TaskAttribute]: date_defns: List[str] = [] # remove date defns iteratively until nothing is left - while (match := date_defn_re.search(title)) is not None: + while (match := task_attr_re.search(title)) is not None: start_idx = title.find("[", match.start()) end_idx = title.find("]", match.start()) date_defns.append(title[start_idx + 1 : end_idx]) From fa10d349b62c7b8112196293fe54d00687319a00 Mon Sep 17 00:00:00 2001 From: Vighnesh Iyer Date: Fri, 15 Mar 2024 18:41:32 -0700 Subject: [PATCH 02/11] wip --- tests/test_parser.py | 138 +++++++++++++++++++++++++++++++------------ today/parser.py | 96 +++++++++++++++--------------- today/task.py | 62 +++++++++++++++++-- 3 files changed, 204 insertions(+), 92 deletions(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index 262ebde..a14bda9 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -2,8 +2,15 @@ from datetime import date from today.task import Task -from today.parser import parse_heading, handle_headings_stack, parse_markdown, extract_date_defns,\ - parse_task_title, Heading +from today.parser import ( + parse_heading, + handle_headings_stack, + parse_markdown, + extract_task_attrs, + parse_task_title, + Heading, +) + class TestParser: def test_parse_heading(self) -> None: @@ -19,8 +26,7 @@ def test_handle_headings_stack(self) -> None: # Test deepening hierarchy assert handle_headings_stack([], "# h1") == ["h1"] - assert handle_headings_stack(['h1', 'h2'], "### h3") == \ - ["h1", "h2", "h3"] + assert handle_headings_stack(["h1", "h2"], "### h3") == ["h1", "h2", "h3"] with pytest.raises(ValueError): handle_headings_stack(["h1", "h2"], "#### h4") @@ -29,34 +35,61 @@ def test_handle_headings_stack(self) -> None: # Test pulling out of hierarchy assert handle_headings_stack(["h1", "h2"], "# h1prime") == ["h1prime"] - assert handle_headings_stack(["h1", "h2", "h3"], "## h2prime") == ["h1", "h2prime"] - assert handle_headings_stack(["h1", "h2", "h3", "h4"], "## h2prime") == ["h1", "h2prime"] - assert handle_headings_stack(["h1", "h2", "h3", "h4"], "# h1prime") == ["h1prime"] + assert handle_headings_stack(["h1", "h2", "h3"], "## h2prime") == [ + "h1", + "h2prime", + ] + assert handle_headings_stack(["h1", "h2", "h3", "h4"], "## h2prime") == [ + "h1", + "h2prime", + ] + assert handle_headings_stack(["h1", "h2", "h3", "h4"], "# h1prime") == [ + "h1prime" + ] - def test_extract_date_defns(self) -> None: - date_defns, title = extract_date_defns("[c:33] things #tag [d:233] [f:5] asdf [c:99]") - assert date_defns == ["c:33", "d:233", "f:5", "c:99"] - assert title == "things #tag asdf" + #def test_extract_date_defns(self) -> None: + # date_defns, title = extract_date_defns( + # "[c:33] things #tag [d:233] [f:5] asdf [c:99]" + # ) + # assert date_defns == ["c:33", "d:233", "f:5", "c:99"] + # assert title == "things #tag asdf" + + # date_defns2, title2 = extract_date_defns( + # "[d:3/3] title [link](http://link.org) [f:3]" + # ) + # assert date_defns2 == ["d:3/3", "f:3"] + # assert title2 == "title [link](http://link.org)" + + def test_extract_task_attrs(self) -> None: - date_defns2, title2 = extract_date_defns("[d:3/3] title [link](http://link.org) [f:3]") - assert date_defns2 == ["d:3/3", "f:3"] - assert title2 == "title [link](http://link.org)" def test_parse_task_title(self) -> None: - assert parse_task_title("[d:1/1/2022] task *title* #tag [c:2/2/2022] other [r:1/4/2022] [f:1/5/2022]", today=date.today()) == \ - Task(title="task *title* #tag other", due_date=date(2022, 1, 1), - created_date=date(2022, 2, 2), reminder_date=date(2022, 1, 4), finished_date=date(2022, 1, 5)) + assert parse_task_title( + "[d:1/1/2022] task *title* #tag [c:2/2/2022] other [r:1/4/2022] [f:1/5/2022]", + today=date.today(), + ) == Task( + title="task *title* #tag other", + due_date=date(2022, 1, 1), + created_date=date(2022, 2, 2), + reminder_date=date(2022, 1, 4), + finished_date=date(2022, 1, 5), + ) with pytest.raises(ValueError): parse_task_title("task [k:1/2/2023]", date.today()) - assert parse_task_title("task [d:t] [r:t]", date.today()) == \ - Task(title="task", due_date=date.today(), reminder_date=date.today()) + assert parse_task_title("task [d:t] [r:t]", date.today()) == Task( + title="task", due_date=date.today(), reminder_date=date.today() + ) def test_basic_task_parsing(self) -> None: - assert parse_markdown(["- [ ] Task 1"]) == [Task(path=[], title="Task 1", done=False, line_number=1)] + assert parse_markdown(["- [ ] Task 1"]) == [ + Task(path=[], title="Task 1", done=False, line_number=1) + ] assert parse_markdown(["# h1", "", "## h2", "", "- [ ] Task 1"]) == [ Task(path=["h1", "h2"], title="Task 1", done=False, line_number=5) ] - assert parse_markdown(["# h1", "", "## h2", "", "- [ ] Task 1", "## h2prime", "", "- [x] Task 2"]) == [ + assert parse_markdown( + ["# h1", "", "## h2", "", "- [ ] Task 1", "## h2prime", "", "- [x] Task 2"] + ) == [ Task(path=["h1", "h2"], title="Task 1", done=False, line_number=5), Task(path=["h1", "h2prime"], title="Task 2", done=True, line_number=8), ] @@ -72,11 +105,17 @@ def test_basic_task_parsing(self) -> None: - [x] Task 2 """ - result = parse_markdown(tasks_with_desc.split('\n'), date.today()) + result = parse_markdown(tasks_with_desc.split("\n"), date.today()) assert result == [ - Task(path=["Tasks"], title="Task 1", done=False, line_number=3, - description="Description for task 1\n\n> quote block\n>\n> another line", due_date=date(2023, 2, 3)), - Task(path=["Tasks"], title="Task 2", done=True, line_number=11) + Task( + path=["Tasks"], + title="Task 1", + done=False, + line_number=3, + description="Description for task 1\n\n> quote block\n>\n> another line", + due_date=date(2023, 2, 3), + ), + Task(path=["Tasks"], title="Task 2", done=True, line_number=11), ] def test_bullet_task_parsing(self) -> None: @@ -90,12 +129,18 @@ def test_bullet_task_parsing(self) -> None: - [ ] Task 4 """ - result = parse_markdown(bullet_tasks.split('\n'), date.today()) + result = parse_markdown(bullet_tasks.split("\n"), date.today()) assert result == [ Task(path=["Tasks"], title="Task 1", done=False, line_number=2), Task(path=["Tasks"], title="Task 2", done=True, line_number=3), - Task(path=["Tasks"], title="Task 3", done=False, description="- Bullets\n - Nested bullets", line_number=4), - Task(path=["Tasks"], title="Task 4", done=False, line_number=9) + Task( + path=["Tasks"], + title="Task 3", + done=False, + description="- Bullets\n - Nested bullets", + line_number=4, + ), + Task(path=["Tasks"], title="Task 4", done=False, line_number=9), ] def test_subtask_parsing(self) -> None: @@ -108,14 +153,33 @@ def test_subtask_parsing(self) -> None: Description """ today = date.today() - result = parse_markdown(subtasks.split('\n'), today) + result = parse_markdown(subtasks.split("\n"), today) assert result == [ - Task(path=["Tasks"], title="Main task", done=False, line_number=3, - description="Description", due_date=date(2022, 1, 10), reminder_date=date(2022, 1, 3), - subtasks=[ - Task(path=["Tasks"], title="Subtask 1", done=False, line_number=4, - due_date=today, reminder_date=date(2022, 1, 3)), - Task(path=["Tasks"], title="Subtask 2", done=False, line_number=5, - due_date=date(2022, 1, 10), reminder_date=date(2022, 1, 3)) - ]) + Task( + path=["Tasks"], + title="Main task", + done=False, + line_number=3, + description="Description", + due_date=date(2022, 1, 10), + reminder_date=date(2022, 1, 3), + subtasks=[ + Task( + path=["Tasks"], + title="Subtask 1", + done=False, + line_number=4, + due_date=today, + reminder_date=date(2022, 1, 3), + ), + Task( + path=["Tasks"], + title="Subtask 2", + done=False, + line_number=5, + due_date=date(2022, 1, 10), + reminder_date=date(2022, 1, 3), + ), + ], + ) ] diff --git a/today/parser.py b/today/parser.py index 9f8ee49..448da2d 100644 --- a/today/parser.py +++ b/today/parser.py @@ -1,10 +1,16 @@ -from abc import ABC -from dataclasses import dataclass from typing import Tuple, List, Optional -from datetime import date, timedelta +from datetime import date import re -from today.task import Task, Heading +from today.task import ( + AssignmentAttribute, + ImportanceAttribute, + Task, + Heading, + RawAttribute, + TaskAttributes, + TaskTitle, +) task_attr_re = re.compile(r"\[.:.") task_re = re.compile(r"^- \[[ xX]\] ") @@ -37,52 +43,44 @@ def handle_headings_stack(headings_stack: List[str], heading_raw: str) -> List[s return headings_stack -@dataclass -class TaskAttribute(ABC): - def is_visible(self, today: date, lookahead_days: int) -> bool: - raise NotImplementedError() - - -@dataclass -class DateAttribute(TaskAttribute): - created_date: Optional[date] - due_date: Optional[date] - reminder_date: Optional[date] - finished_date: Optional[date] - - # today = 3, due_date = 5 (not visible) - # today = 5, due_date = 5 (visible) - # today = 3, due_date = 5, lookahead_days = 1 (not visible) - # today = 3, due_date = 5, lookahead_days = 2 (visible) - def is_visible(self, today: date, lookahead_days: int) -> bool: - effective_date = today + timedelta(days=lookahead_days) - if self.due_date and effective_date >= self.due_date: - return True - elif self.reminder_date and effective_date >= self.reminder_date: - return True - else: - return False - - -@dataclass -class AssignmentAttribute(TaskAttribute): - assigned_to: str - +def extract_task_attrs(raw_task_title: str) -> Tuple[TaskAttributes, TaskTitle]: + task_attr = TaskAttributes() -def extract_task_attrs(task_title: str) -> List[TaskAttribute]: - pass - - -def extract_task_attrs(title: str) -> List[TaskAttribute]: - date_defns: List[str] = [] - - # remove date defns iteratively until nothing is left - while (match := task_attr_re.search(title)) is not None: - start_idx = title.find("[", match.start()) - end_idx = title.find("]", match.start()) - date_defns.append(title[start_idx + 1 : end_idx]) - title = title.replace(title[start_idx : end_idx + 2], "") - return date_defns, title.rstrip() + # remove attributes from the [raw_task_title] iteratively until no more are left + while (match := task_attr_re.search(raw_task_title)) is not None: + start_idx = raw_task_title.find("[", match.start()) + end_idx = raw_task_title.find("]", match.start()) + attr_string = raw_task_title[start_idx + 1 : end_idx] + if attr_string[0] == "@": + task_attr.assn_attr = AssignmentAttribute(attr_string[1:]) + elif attr_string[0] == "!": + task_attr.importance_attr = ImportanceAttribute(int(attr_string[1:])) + else: + # This must be a date attribute + attr_string_parts = attr_string.split(":") + assert ( + len(attr_string_parts) == 2 + ), f"Date attribute '{attr_string}' from task title '{raw_task_title}' is improperly formatted" + prefix = attr_string_parts[0] + date_parts = attr_string_parts[1].split('/') + if prefix == "c": + + raw_attributes.append(raw_task_title[start_idx + 1 : end_idx]) + # Remove the parsed attribute from the title + raw_task_title = raw_task_title.replace(raw_task_title[start_idx : end_idx + 2], "") + return raw_attributes, raw_task_title.rstrip() + + +# def extract_task_defns(title: str) -> List[TaskAttribute]: +# date_defns: List[str] = [] +# +# # remove date defns iteratively until nothing is left +# while (match := task_attr_re.search(title)) is not None: +# start_idx = title.find("[", match.start()) +# end_idx = title.find("]", match.start()) +# date_defns.append(title[start_idx + 1 : end_idx]) +# title = title.replace(title[start_idx : end_idx + 2], "") +# return date_defns, title.rstrip() def parse_task_title(title: str, today: date) -> Task: diff --git a/today/task.py b/today/task.py index f51d4af..c4f5aaa 100644 --- a/today/task.py +++ b/today/task.py @@ -4,6 +4,62 @@ from pathlib import Path +@dataclass +class Heading: + level: int + name: str + + +@dataclass +class RawAttribute: + prefix: str + value: str + + +TaskTitle = str + + +@dataclass +class DateAttribute: + created_date: Optional[date] = None + due_date: Optional[date] = None + reminder_date: Optional[date] = None + finished_date: Optional[date] = None + + # today = 3, due_date = 5 (not visible) + # today = 5, due_date = 5 (visible) + # today = 3, due_date = 5, lookahead_days = 1 (not visible) + # today = 3, due_date = 5, lookahead_days = 2 (visible) + def is_visible(self, today: date, lookahead_days: int) -> bool: + effective_date = today + timedelta(days=lookahead_days) + if self.due_date and effective_date >= self.due_date: + return True + elif self.reminder_date and effective_date >= self.reminder_date: + return True + else: + return False + + +@dataclass +class AssignmentAttribute: + assigned_to: str + + +@dataclass +class ImportanceAttribute: + importance: int + + +@dataclass +class TaskAttributes: + date_attr: DateAttribute = DateAttribute() + assn_attr: Optional[AssignmentAttribute] = None + importance_attr: Optional[ImportanceAttribute] = None + + def is_visible(self, today: date, lookahead_days: int) -> bool: + raise NotImplementedError() + + def date_relative_to_today(d: date, today: date, prefix: str = "") -> str: if d < today: delta: timedelta = today - d @@ -22,12 +78,6 @@ def days(days: timedelta) -> str: return f"{days.days} days" -@dataclass -class Heading: - level: int - name: str - - @dataclass class Task: path: List[str] = field(default_factory=lambda: []) From 7729c7ce5376d76d3d195ac541873cb04efe0410 Mon Sep 17 00:00:00 2001 From: Vighnesh Iyer Date: Sun, 17 Mar 2024 15:31:14 -0700 Subject: [PATCH 03/11] more wip --- poetry.lock | 111 ++++++++++++--------------- pyproject.toml | 4 +- tests/test_parser.py | 24 +++++- today/parser.py | 178 +++++++++++++++++++++++++++---------------- today/task.py | 13 ++-- 5 files changed, 191 insertions(+), 139 deletions(-) diff --git a/poetry.lock b/poetry.lock index c31fb4e..fea9de9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -83,65 +83,18 @@ files = [ ] [[package]] -name = "mypy" -version = "0.991" -description = "Optional static typing for Python" +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" optional = false -python-versions = ">=3.7" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ - {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"}, - {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"}, - {file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"}, - {file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"}, - {file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"}, - {file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"}, - {file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"}, - {file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"}, - {file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"}, - {file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"}, - {file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"}, - {file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"}, - {file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"}, - {file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"}, - {file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"}, - {file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"}, - {file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"}, - {file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"}, - {file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"}, - {file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"}, - {file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"}, - {file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"}, - {file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"}, - {file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"}, - {file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"}, - {file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"}, - {file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"}, - {file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"}, - {file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"}, - {file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"}, + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, ] [package.dependencies] -mypy-extensions = ">=0.4.3" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=3.10" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -python2 = ["typed-ast (>=1.4.0,<2)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] +setuptools = "*" [[package]] name = "packaging" @@ -294,15 +247,33 @@ files = [ plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyright" +version = "1.1.354" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.354-py3-none-any.whl", hash = "sha256:f28d61ae8ae035fc52ded1070e8d9e786051a26a4127bbd7a4ba0399b81b37b5"}, + {file = "pyright-1.1.354.tar.gz", hash = "sha256:b1070dc774ff2e79eb0523fe87f4ba9a90550de7e4b030a2bc9e031864029a1f"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + [[package]] name = "pytest" -version = "7.4.4" +version = "8.1.1" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, + {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, + {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, ] [package.dependencies] @@ -310,11 +281,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.4,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "rich" @@ -334,6 +305,22 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "setuptools" +version = "69.2.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, + {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "tomli" version = "2.0.1" @@ -359,4 +346,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "dfdec76d39d0f469be30b56cc3f7f3f6c5785db2f07525fdbc399e1457d88a60" +content-hash = "aa0ce2407c2d933354223951cd6516cd00aa5835057eda331954ef4f020f315b" diff --git a/pyproject.toml b/pyproject.toml index 6d25d43..97ba96d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,8 @@ rich = "^13.7" pydantic = "^2.6.4" [tool.poetry.dev-dependencies] -pytest = "^7.1" -mypy = "^0.991" +pytest = "^8.1" +pyright = "^1.1" [[tool.poetry.source]] name = "PyPI" diff --git a/tests/test_parser.py b/tests/test_parser.py index a14bda9..beca8fa 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -13,6 +13,8 @@ class TestParser: + today = date.today() + def test_parse_heading(self) -> None: assert parse_heading("# Title") == Heading(1, "Title") assert parse_heading("### **title 2**") == Heading(3, "**title 2**") @@ -47,7 +49,7 @@ def test_handle_headings_stack(self) -> None: "h1prime" ] - #def test_extract_date_defns(self) -> None: + # def test_extract_date_defns(self) -> None: # date_defns, title = extract_date_defns( # "[c:33] things #tag [d:233] [f:5] asdf [c:99]" # ) @@ -61,7 +63,25 @@ def test_handle_headings_stack(self) -> None: # assert title2 == "title [link](http://link.org)" def test_extract_task_attrs(self) -> None: - + attrs, title = extract_task_attrs( + raw_task_title="[c:3/4/2024] things #tag [d:3/8/2024] [f:t] asdf", + today=self.today, + ) + assert attrs.date_attr.created_date == date(2024, 3, 4) + assert attrs.date_attr.due_date == date(2024, 3, 8) + assert attrs.date_attr.finished_date == self.today + assert title == "things #tag asdf" + + attrs2, title2 = extract_task_attrs( + raw_task_title="[d:3/3] title [link](http://link.org) [@:vighneshiyer] [!:2]", + today=self.today, + ) + assert attrs2.date_attr.due_date == date(self.today.year, 3, 3) + assert attrs2.assn_attr + assert attrs2.assn_attr.assigned_to == "vighneshiyer" + assert attrs2.priority_attr + assert attrs2.priority_attr.priority == 2 + assert title2 == "title [link](http://link.org)" def test_parse_task_title(self) -> None: assert parse_task_title( diff --git a/today/parser.py b/today/parser.py index 448da2d..ee21bea 100644 --- a/today/parser.py +++ b/today/parser.py @@ -4,10 +4,9 @@ from today.task import ( AssignmentAttribute, - ImportanceAttribute, + PriorityAttribute, Task, Heading, - RawAttribute, TaskAttributes, TaskTitle, ) @@ -28,6 +27,8 @@ def parse_heading(s: str) -> Heading: raise ValueError("This should never happen") +# Given the current state of headings in [headings_stack] and a new heading [heading_raw], +# validate that the new heading has the correct indentation and return a new heading stack def handle_headings_stack(headings_stack: List[str], heading_raw: str) -> List[str]: heading = parse_heading(heading_raw) last_level = len(headings_stack) @@ -43,7 +44,66 @@ def handle_headings_stack(headings_stack: List[str], heading_raw: str) -> List[s return headings_stack -def extract_task_attrs(raw_task_title: str) -> Tuple[TaskAttributes, TaskTitle]: +def md_checkbox(s: str) -> Optional[bool]: + # None = not a checkbox, True = checked, False = unchecked + if s.startswith("[ ]"): + return False + elif s.startswith("[x]") or s.startswith("[X]"): + return True + else: + return None + + +# Mutates the fields of [task_attr] based on the [attr_string] +# [attr_string] is of the form [d:] or [@person] () or [!2] (priority flag) +# If the [attr_string] is malformed, return an error message +def assign_task_attr( + attr_string: str, task_attr: TaskAttributes, today: date +) -> None | str: + if attr_string[:2] == "@:": + # This is an assignment attribute + task_attr.assn_attr = AssignmentAttribute(attr_string[2:]) + return + elif attr_string[:2] == "!:": + # This is a priority attribute + task_attr.priority_attr = PriorityAttribute(int(attr_string[2:])) + return + else: + # This must be a date attribute + assert attr_string[1] == ":" + prefix = attr_string[0] + date_raw = attr_string[2:] + date_value: date + if date_raw == "t": + date_value = today + else: + date_split = [int(d) for d in date_raw.split("/")] + if len(date_split) == 3: # month / day / year + date_value = date( + year=date_split[2], month=date_split[0], day=date_split[1] + ) + elif len(date_split) == 2: # month / day (year is implicitly today's year) + date_value = date( + year=today.year, month=date_split[0], day=date_split[1] + ) + else: + return f"Date attribute '{attr_string}' is improperly formatted" + if prefix == "c": + task_attr.date_attr.created_date = date_value + elif prefix == "d": + task_attr.date_attr.due_date = date_value + elif prefix == "r": + task_attr.date_attr.reminder_date = date_value + elif prefix == "f": + task_attr.date_attr.finished_date = date_value + else: + return f"Date attribute prefix '{prefix}' in date attribute '{attr_string}' isn't recognized" + return + + +def extract_task_attrs( + raw_task_title: str, today: date +) -> Tuple[TaskAttributes, TaskTitle]: task_attr = TaskAttributes() # remove attributes from the [raw_task_title] iteratively until no more are left @@ -51,24 +111,22 @@ def extract_task_attrs(raw_task_title: str) -> Tuple[TaskAttributes, TaskTitle]: start_idx = raw_task_title.find("[", match.start()) end_idx = raw_task_title.find("]", match.start()) attr_string = raw_task_title[start_idx + 1 : end_idx] - if attr_string[0] == "@": - task_attr.assn_attr = AssignmentAttribute(attr_string[1:]) - elif attr_string[0] == "!": - task_attr.importance_attr = ImportanceAttribute(int(attr_string[1:])) - else: - # This must be a date attribute - attr_string_parts = attr_string.split(":") - assert ( - len(attr_string_parts) == 2 - ), f"Date attribute '{attr_string}' from task title '{raw_task_title}' is improperly formatted" - prefix = attr_string_parts[0] - date_parts = attr_string_parts[1].split('/') - if prefix == "c": - - raw_attributes.append(raw_task_title[start_idx + 1 : end_idx]) + error_msg = assign_task_attr(attr_string, task_attr, today) + if error_msg is not None: + raise RuntimeError( + f"An error was encountered when parsing the task title '{raw_task_title}'. Error: {error_msg}" + ) # Remove the parsed attribute from the title - raw_task_title = raw_task_title.replace(raw_task_title[start_idx : end_idx + 2], "") - return raw_attributes, raw_task_title.rstrip() + raw_task_title = raw_task_title.replace( + raw_task_title[start_idx : end_idx + 2], "" + ) + return task_attr, raw_task_title.rstrip() + + +def parse_task_title(title: str, today: date) -> Task: + task_attr, task_title = extract_task_attrs(title, today) + t = Task(title=task_title, attrs=task_attr) + return t # def extract_task_defns(title: str) -> List[TaskAttribute]: @@ -83,51 +141,41 @@ def extract_task_attrs(raw_task_title: str) -> Tuple[TaskAttributes, TaskTitle]: # return date_defns, title.rstrip() -def parse_task_title(title: str, today: date) -> Task: - date_defns, task_title = extract_date_defns(title) - t = Task(title=task_title) - for defn in date_defns: - prefix = defn[0] - assert defn[1] == ":" - if defn[2:] == "t": - date_value = today - else: - date_split = [int(d) for d in defn[2:].split("/")] - if len(date_split) == 3: - date_value = date( - year=date_split[2], month=date_split[0], day=date_split[1] - ) - elif len(date_split) == 2: - date_value = date( - year=today.year, month=date_split[0], day=date_split[1] - ) - else: - raise ValueError( - f"Unable to parse date for task {title} and date string {defn}" - ) - if prefix == "c": - t.created_date = date_value - elif prefix == "r": - t.reminder_date = date_value - elif prefix == "d": - t.due_date = date_value - elif prefix == "f": - t.finished_date = date_value - else: - raise ValueError( - f"Prefix {prefix} in date definition string {defn} is not recognized" - ) - return t - - -def md_checkbox(s: str) -> Optional[bool]: - # None = not a checkbox, True = checked, False = unchecked - if s.startswith("[ ]"): - return False - elif s.startswith("[x]") or s.startswith("[X]"): - return True - else: - return None +# def parse_task_title(title: str, today: date) -> Task: +# date_defns, task_title = extract_date_defns(title) +# t = Task(title=task_title) +# for defn in date_defns: +# prefix = defn[0] +# assert defn[1] == ":" +# if defn[2:] == "t": +# date_value = today +# else: +# date_split = [int(d) for d in defn[2:].split("/")] +# if len(date_split) == 3: +# date_value = date( +# year=date_split[2], month=date_split[0], day=date_split[1] +# ) +# elif len(date_split) == 2: +# date_value = date( +# year=today.year, month=date_split[0], day=date_split[1] +# ) +# else: +# raise ValueError( +# f"Unable to parse date for task {title} and date string {defn}" +# ) +# if prefix == "c": +# t.created_date = date_value +# elif prefix == "r": +# t.reminder_date = date_value +# elif prefix == "d": +# t.due_date = date_value +# elif prefix == "f": +# t.finished_date = date_value +# else: +# raise ValueError( +# f"Prefix {prefix} in date definition string {defn} is not recognized" +# ) +# return t def parse_markdown(md: List[str], today: date = date.today()) -> List[Task]: diff --git a/today/task.py b/today/task.py index c4f5aaa..1f79c43 100644 --- a/today/task.py +++ b/today/task.py @@ -46,15 +46,15 @@ class AssignmentAttribute: @dataclass -class ImportanceAttribute: - importance: int +class PriorityAttribute: + priority: int @dataclass class TaskAttributes: - date_attr: DateAttribute = DateAttribute() + date_attr: DateAttribute = field(default_factory=lambda: DateAttribute()) assn_attr: Optional[AssignmentAttribute] = None - importance_attr: Optional[ImportanceAttribute] = None + priority_attr: Optional[PriorityAttribute] = None def is_visible(self, today: date, lookahead_days: int) -> bool: raise NotImplementedError() @@ -85,10 +85,7 @@ class Task: done: bool = False description: str = "" # A Markdown string with the task description subtasks: List["Task"] = field(default_factory=lambda: []) - created_date: Optional[date] = None - reminder_date: Optional[date] = None - due_date: Optional[date] = None - finished_date: Optional[date] = None + attrs: TaskAttributes = field(default_factory=lambda: TaskAttributes()) file_path: Path = Path.cwd() line_number: int = 0 From 9f052042d99ae6fa9b1ba8e2bc584e0a8de8b4d2 Mon Sep 17 00:00:00 2001 From: Vighnesh Iyer Date: Sun, 17 Mar 2024 19:08:35 -0700 Subject: [PATCH 04/11] more wip --- poetry.lock | 13 ++++- pyproject.toml | 1 + tests/test_parser.py | 29 ++++++----- today-tasks.md | 4 +- today/parser.py | 114 +++++++++++++++---------------------------- today/task.py | 1 + 6 files changed, 71 insertions(+), 91 deletions(-) diff --git a/poetry.lock b/poetry.lock index fea9de9..af420b5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -82,6 +82,17 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "more-itertools" +version = "10.2.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1"}, + {file = "more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684"}, +] + [[package]] name = "nodeenv" version = "1.8.0" @@ -346,4 +357,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "aa0ce2407c2d933354223951cd6516cd00aa5835057eda331954ef4f020f315b" +content-hash = "1af5bf01f5a1421702bb0d4b34812a95f8ae950edf6a62f5343fb1e43227e3ef" diff --git a/pyproject.toml b/pyproject.toml index 97ba96d..2d4e0d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ start = "today.scripts.start:main" python = "^3.9" rich = "^13.7" pydantic = "^2.6.4" +more-itertools = "^10.2" [tool.poetry.dev-dependencies] pytest = "^8.1" diff --git a/tests/test_parser.py b/tests/test_parser.py index beca8fa..99bc2c3 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,7 +1,7 @@ import pytest from datetime import date -from today.task import Task +from today.task import DateAttribute, Task from today.parser import ( parse_heading, handle_headings_stack, @@ -49,20 +49,8 @@ def test_handle_headings_stack(self) -> None: "h1prime" ] - # def test_extract_date_defns(self) -> None: - # date_defns, title = extract_date_defns( - # "[c:33] things #tag [d:233] [f:5] asdf [c:99]" - # ) - # assert date_defns == ["c:33", "d:233", "f:5", "c:99"] - # assert title == "things #tag asdf" - - # date_defns2, title2 = extract_date_defns( - # "[d:3/3] title [link](http://link.org) [f:3]" - # ) - # assert date_defns2 == ["d:3/3", "f:3"] - # assert title2 == "title [link](http://link.org)" - def test_extract_task_attrs(self) -> None: + # Simple attributes attrs, title = extract_task_attrs( raw_task_title="[c:3/4/2024] things #tag [d:3/8/2024] [f:t] asdf", today=self.today, @@ -72,8 +60,9 @@ def test_extract_task_attrs(self) -> None: assert attrs.date_attr.finished_date == self.today assert title == "things #tag asdf" + # Difficult attributes attrs2, title2 = extract_task_attrs( - raw_task_title="[d:3/3] title [link](http://link.org) [@:vighneshiyer] [!:2]", + raw_task_title="title [d:3/3] [link](http://link.org) [@vighneshiyer] [!2]", today=self.today, ) assert attrs2.date_attr.due_date == date(self.today.year, 3, 3) @@ -83,6 +72,16 @@ def test_extract_task_attrs(self) -> None: assert attrs2.priority_attr.priority == 2 assert title2 == "title [link](http://link.org)" + # No attributes + attrs3, title3 = extract_task_attrs( + raw_task_title="things #tag", + today=self.today, + ) + assert attrs3.priority_attr is None + assert attrs3.date_attr == DateAttribute() # empty attribute class + assert attrs3.assn_attr is None + assert title3 == "things #tag" + def test_parse_task_title(self) -> None: assert parse_task_title( "[d:1/1/2022] task *title* #tag [c:2/2/2022] other [r:1/4/2022] [f:1/5/2022]", diff --git a/today-tasks.md b/today-tasks.md index 284770e..6f0d024 100644 --- a/today-tasks.md +++ b/today-tasks.md @@ -37,11 +37,13 @@ The idea is that every day there might be 1-3 critical tasks that should be placed at the top no matter what the heading order is. There should be a way to mark those critical tasks such that they always show up at the top and it makes task selection easy. Also, the remaining tasks can be displayed in the normal tree form as 'extras' for a given day. +I'm generalizing this into a priority marking system. - [ ] Importance markers [d:3/12/2024] - - [ ] Rename date_defns to task_attrs + - [x] Rename date_defns to task_attrs - [ ] Add importance markers parsing - [ ] Add new logic to tree printing + - [ ] Add README docs ### Assignees diff --git a/today/parser.py b/today/parser.py index ee21bea..5053d0e 100644 --- a/today/parser.py +++ b/today/parser.py @@ -1,6 +1,7 @@ from typing import Tuple, List, Optional from datetime import date import re +from more_itertools import windowed from today.task import ( AssignmentAttribute, @@ -11,7 +12,7 @@ TaskTitle, ) -task_attr_re = re.compile(r"\[.:.") +task_attr_re = re.compile(r"\[(?P(.:|@|!))(?P.*?)\]\s?") task_re = re.compile(r"^- \[[ xX]\] ") subtask_re = re.compile(r"^[ \t]+- \[[ xX]\] ") @@ -54,25 +55,24 @@ def md_checkbox(s: str) -> Optional[bool]: return None -# Mutates the fields of [task_attr] based on the [attr_string] -# [attr_string] is of the form [d:] or [@person] () or [!2] (priority flag) -# If the [attr_string] is malformed, return an error message +# Mutates the fields of [task_attr] based on a raw attribute string (prefix + value) +# of the form [d:] (prefix='d:', value='') or [@person] or [!2] +# If the prefix or value are malformed, return an error message def assign_task_attr( - attr_string: str, task_attr: TaskAttributes, today: date + prefix: str, value: str, task_attr: TaskAttributes, today: date ) -> None | str: - if attr_string[:2] == "@:": + if prefix == "@": # This is an assignment attribute - task_attr.assn_attr = AssignmentAttribute(attr_string[2:]) + task_attr.assn_attr = AssignmentAttribute(value) return - elif attr_string[:2] == "!:": + elif prefix == "!": # This is a priority attribute - task_attr.priority_attr = PriorityAttribute(int(attr_string[2:])) + task_attr.priority_attr = PriorityAttribute(int(value)) return else: # This must be a date attribute - assert attr_string[1] == ":" - prefix = attr_string[0] - date_raw = attr_string[2:] + prefix = prefix[0] # the raw prefix passed is of the form 'd:' + date_raw = value date_value: date if date_raw == "t": date_value = today @@ -87,7 +87,7 @@ def assign_task_attr( year=today.year, month=date_split[0], day=date_split[1] ) else: - return f"Date attribute '{attr_string}' is improperly formatted" + return f"Date attribute value '{value}' is improperly formatted" if prefix == "c": task_attr.date_attr.created_date = date_value elif prefix == "d": @@ -97,7 +97,7 @@ def assign_task_attr( elif prefix == "f": task_attr.date_attr.finished_date = date_value else: - return f"Date attribute prefix '{prefix}' in date attribute '{attr_string}' isn't recognized" + return f"Date attribute prefix '{prefix}' isn't recognized" return @@ -106,21 +106,36 @@ def extract_task_attrs( ) -> Tuple[TaskAttributes, TaskTitle]: task_attr = TaskAttributes() - # remove attributes from the [raw_task_title] iteratively until no more are left - while (match := task_attr_re.search(raw_task_title)) is not None: - start_idx = raw_task_title.find("[", match.start()) - end_idx = raw_task_title.find("]", match.start()) - attr_string = raw_task_title[start_idx + 1 : end_idx] - error_msg = assign_task_attr(attr_string, task_attr, today) + # Find all matches for the attribute regex and parse each one while mutating [task_attr] + task_attr_matches = list(re.finditer(task_attr_re, raw_task_title)) + if len(task_attr_matches) == 0: + # short circuit when there are no attributes to parse + return task_attr, raw_task_title + for match in task_attr_matches: + prefix = match.group("prefix") + value = match.group("value") + error_msg = assign_task_attr(prefix, value, task_attr, today) if error_msg is not None: raise RuntimeError( f"An error was encountered when parsing the task title '{raw_task_title}'. Error: {error_msg}" ) - # Remove the parsed attribute from the title - raw_task_title = raw_task_title.replace( - raw_task_title[start_idx : end_idx + 2], "" - ) - return task_attr, raw_task_title.rstrip() + + # Strip all the task attribute matches from the [raw_task_title] + # Do this efficiently by first computing the substring indices we need, then constructing the new string + match_spans: List[Tuple[int, int]] = [m.span(0) for m in task_attr_matches] + not_match_spans = ( + [(0, match_spans[0][0])] + + [ + (span1[1], span2[0]) + for (span1, span2) in windowed(match_spans, 2) + if span1 is not None and span2 is not None + ] + + [(match_spans[-1][1], len(raw_task_title))] + ) + task_title = "" + for span in not_match_spans: + task_title = task_title + raw_task_title[span[0] : span[1]] + return task_attr, task_title.rstrip() def parse_task_title(title: str, today: date) -> Task: @@ -129,55 +144,6 @@ def parse_task_title(title: str, today: date) -> Task: return t -# def extract_task_defns(title: str) -> List[TaskAttribute]: -# date_defns: List[str] = [] -# -# # remove date defns iteratively until nothing is left -# while (match := task_attr_re.search(title)) is not None: -# start_idx = title.find("[", match.start()) -# end_idx = title.find("]", match.start()) -# date_defns.append(title[start_idx + 1 : end_idx]) -# title = title.replace(title[start_idx : end_idx + 2], "") -# return date_defns, title.rstrip() - - -# def parse_task_title(title: str, today: date) -> Task: -# date_defns, task_title = extract_date_defns(title) -# t = Task(title=task_title) -# for defn in date_defns: -# prefix = defn[0] -# assert defn[1] == ":" -# if defn[2:] == "t": -# date_value = today -# else: -# date_split = [int(d) for d in defn[2:].split("/")] -# if len(date_split) == 3: -# date_value = date( -# year=date_split[2], month=date_split[0], day=date_split[1] -# ) -# elif len(date_split) == 2: -# date_value = date( -# year=today.year, month=date_split[0], day=date_split[1] -# ) -# else: -# raise ValueError( -# f"Unable to parse date for task {title} and date string {defn}" -# ) -# if prefix == "c": -# t.created_date = date_value -# elif prefix == "r": -# t.reminder_date = date_value -# elif prefix == "d": -# t.due_date = date_value -# elif prefix == "f": -# t.finished_date = date_value -# else: -# raise ValueError( -# f"Prefix {prefix} in date definition string {defn} is not recognized" -# ) -# return t - - def parse_markdown(md: List[str], today: date = date.today()) -> List[Task]: headings_stack: List[str] = [] current_task: Optional[Task] = None diff --git a/today/task.py b/today/task.py index 1f79c43..c603f27 100644 --- a/today/task.py +++ b/today/task.py @@ -47,6 +47,7 @@ class AssignmentAttribute: @dataclass class PriorityAttribute: + # [priority] of 0 is higher than [priority] of 1 priority: int From 1143f4264d0576f4b441376822c3d97a6d2dae60 Mon Sep 17 00:00:00 2001 From: Vighnesh Iyer Date: Mon, 18 Mar 2024 18:33:20 -0700 Subject: [PATCH 05/11] more wip --- tests/test_parser.py | 47 ++++++++++++----- tests/test_task.py | 118 ++++++++++++++++++++++++------------------- today-tasks.md | 2 +- today/parser.py | 13 ++--- today/task.py | 49 +++++++++--------- 5 files changed, 126 insertions(+), 103 deletions(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index 99bc2c3..997922e 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,7 +1,7 @@ import pytest from datetime import date -from today.task import DateAttribute, Task +from today.task import DateAttribute, Task, TaskAttributes, date_relative_to_today from today.parser import ( parse_heading, handle_headings_stack, @@ -88,15 +88,24 @@ def test_parse_task_title(self) -> None: today=date.today(), ) == Task( title="task *title* #tag other", - due_date=date(2022, 1, 1), - created_date=date(2022, 2, 2), - reminder_date=date(2022, 1, 4), - finished_date=date(2022, 1, 5), + attrs=TaskAttributes( + date_attr=DateAttribute( + created_date=date(2022, 2, 2), + due_date=date(2022, 1, 1), + reminder_date=date(2022, 1, 4), + finished_date=date(2022, 1, 5), + ) + ), ) - with pytest.raises(ValueError): + with pytest.raises(RuntimeError): parse_task_title("task [k:1/2/2023]", date.today()) assert parse_task_title("task [d:t] [r:t]", date.today()) == Task( - title="task", due_date=date.today(), reminder_date=date.today() + title="task", + attrs=TaskAttributes( + date_attr=DateAttribute( + due_date=date.today(), reminder_date=date.today() + ) + ), ) def test_basic_task_parsing(self) -> None: @@ -132,7 +141,7 @@ def test_basic_task_parsing(self) -> None: done=False, line_number=3, description="Description for task 1\n\n> quote block\n>\n> another line", - due_date=date(2023, 2, 3), + attrs=TaskAttributes(DateAttribute(due_date=date(2023, 2, 3))), ), Task(path=["Tasks"], title="Task 2", done=True, line_number=11), ] @@ -180,24 +189,34 @@ def test_subtask_parsing(self) -> None: done=False, line_number=3, description="Description", - due_date=date(2022, 1, 10), - reminder_date=date(2022, 1, 3), + attrs=TaskAttributes( + DateAttribute( + due_date=date(2022, 1, 10), reminder_date=date(2022, 1, 3) + ) + ), subtasks=[ Task( path=["Tasks"], title="Subtask 1", done=False, line_number=4, - due_date=today, - reminder_date=date(2022, 1, 3), + attrs=TaskAttributes( + DateAttribute( + due_date=today, reminder_date=date(2022, 1, 3) + ) + ), ), Task( path=["Tasks"], title="Subtask 2", done=False, line_number=5, - due_date=date(2022, 1, 10), - reminder_date=date(2022, 1, 3), + attrs=TaskAttributes( + DateAttribute( + due_date=date(2022, 1, 10), + reminder_date=date(2022, 1, 3), + ) + ), ), ], ) diff --git a/tests/test_task.py b/tests/test_task.py index ffa3a8e..341d79d 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -1,8 +1,9 @@ from datetime import date import unicodedata import functools +from dataclasses import replace -from today.task import Task, task_sorter +from today.task import DateAttribute, Task, TaskAttributes, task_sorter def remove_control_characters(s): @@ -11,56 +12,60 @@ def remove_control_characters(s): class TestTask: def test_task_is_displayed(self) -> None: - assert Task(due_date=date(2022, 1, 5)).is_displayed(date(2022, 1, 4)) is False - assert Task(due_date=date(2022, 1, 5)).is_displayed(date(2022, 1, 5)) is True - assert Task(due_date=date(2022, 1, 5)).is_displayed(date(2022, 1, 6)) is True - assert Task(due_date=date(2022, 1, 5), done=True).is_displayed(date(2022, 1, 6)) is False - assert Task(due_date=date(2022, 1, 5), reminder_date=date(2022, 1, 3)).is_displayed(date(2022, 1, 1)) is False - assert Task(due_date=date(2022, 1, 5), reminder_date=date(2022, 1, 3)).is_displayed(date(2022, 1, 3)) is True - assert Task(due_date=date(2022, 1, 5), reminder_date=date(2022, 1, 3)).is_displayed(date(2022, 1, 4)) is True - assert Task(due_date=date(2022, 1, 5), reminder_date=date(2022, 1, 3), done=True).is_displayed(date(2022, 1, 4)) is False + task = Task(attrs=TaskAttributes(DateAttribute(due_date=date(2022, 1, 5)))) + task_reminder = Task( + attrs=TaskAttributes( + DateAttribute(due_date=date(2022, 1, 5), reminder_date=date(2022, 1, 3)) + ) + ) + assert task.is_displayed(date(2022, 1, 4)) is False + assert task.is_displayed(date(2022, 1, 5)) is True + assert task.is_displayed(date(2022, 1, 6)) is True + assert replace(task, done=True).is_displayed(date(2022, 1, 6)) is False + assert task_reminder.is_displayed(date(2022, 1, 1)) is False + assert task_reminder.is_displayed(date(2022, 1, 3)) + assert task_reminder.is_displayed(date(2022, 1, 4)) is True + assert replace(task_reminder, done=True).is_displayed(date(2022, 1, 4)) is False def test_task_summary(self) -> None: # Task with only a due date - task = Task(due_date=date(2022, 1, 5), title="Task 1") - assert task.summary(today=date(2022, 1, 5)) == \ - "[**Due today**]" - assert task.summary(today=date(2022, 1, 4)) ==\ - "[Due in 1 day]" - assert task.summary(today=date(2022, 1, 1)) == \ - "[Due in 4 days]" - assert task.summary(today=date(2022, 1, 6)) == \ - "[**Due 1 day ago**]" - assert task.summary(today=date(2022, 1, 10)) == \ - "[**Due 5 days ago**]" + task = Task( + attrs=TaskAttributes(DateAttribute(due_date=date(2022, 1, 5))), + title="Task 1", + ) + assert task.summary(today=date(2022, 1, 5)) == "[**Due today**]" + assert task.summary(today=date(2022, 1, 4)) == "[Due in 1 day]" + assert task.summary(today=date(2022, 1, 1)) == "[Due in 4 days]" + assert task.summary(today=date(2022, 1, 6)) == "[**Due 1 day ago**]" + assert task.summary(today=date(2022, 1, 10)) == "[**Due 5 days ago**]" # Task with only reminder date task = Task(reminder_date=date(2022, 1, 5), title="Task 1") - assert task.summary(today=date(2022, 1, 5)) == \ - "[**Reminder today**]" - assert task.summary(today=date(2022, 1, 4)) == \ - "[Reminder in 1 day]" - assert task.summary(today=date(2022, 1, 1)) == \ - "[Reminder in 4 days]" - assert task.summary(today=date(2022, 1, 6)) == \ - "[**Reminder 1 day ago**]" - assert task.summary(today=date(2022, 1, 10)) == \ - "[**Reminder 5 days ago**]" + assert task.summary(today=date(2022, 1, 5)) == "[**Reminder today**]" + assert task.summary(today=date(2022, 1, 4)) == "[Reminder in 1 day]" + assert task.summary(today=date(2022, 1, 1)) == "[Reminder in 4 days]" + assert task.summary(today=date(2022, 1, 6)) == "[**Reminder 1 day ago**]" + assert task.summary(today=date(2022, 1, 10)) == "[**Reminder 5 days ago**]" # Task with due and reminder dates - task = Task(due_date=date(2022, 1, 5), reminder_date=date(2022, 1, 1), title="Task 1") - assert task.summary(today=date(2022, 1, 5)) == \ - "[**Due today**]" - assert task.summary(today=date(2022, 1, 4)) == \ - "[**Reminder 3 days ago**] [Due in 1 day]" - assert task.summary(today=date(2022, 1, 1)) == \ - "[**Reminder today**] [Due in 4 days]" - assert task.summary(today=date(2021, 12, 31)) == \ - "[Reminder in 1 day] [Due in 5 days]" - assert task.summary(today=date(2022, 1, 6)) == \ - "[**Due 1 day ago**]" - assert task.summary(today=date(2022, 1, 10)) == \ - "[**Due 5 days ago**]" + task = Task( + due_date=date(2022, 1, 5), reminder_date=date(2022, 1, 1), title="Task 1" + ) + assert task.summary(today=date(2022, 1, 5)) == "[**Due today**]" + assert ( + task.summary(today=date(2022, 1, 4)) + == "[**Reminder 3 days ago**] [Due in 1 day]" + ) + assert ( + task.summary(today=date(2022, 1, 1)) + == "[**Reminder today**] [Due in 4 days]" + ) + assert ( + task.summary(today=date(2021, 12, 31)) + == "[Reminder in 1 day] [Due in 5 days]" + ) + assert task.summary(today=date(2022, 1, 6)) == "[**Due 1 day ago**]" + assert task.summary(today=date(2022, 1, 10)) == "[**Due 5 days ago**]" def test_task_sorting(self) -> None: today = date(2022, 1, 6) @@ -78,15 +83,22 @@ def test_task_sorting(self) -> None: # 5. tasks with due/reminder dates in the future # Task due at 1/5 is past due vs the task due at 1/6 (due today) - assert sorted([due_1_5, due_1_6], key=functools.partial(task_sorter, today=today)) == \ - [due_1_5, due_1_6] - assert sorted([due_1_6, due_1_5], key=functools.partial(task_sorter, today=today)) == \ - [due_1_5, due_1_6] + assert sorted( + [due_1_5, due_1_6], key=functools.partial(task_sorter, today=today) + ) == [due_1_5, due_1_6] + assert sorted( + [due_1_6, due_1_5], key=functools.partial(task_sorter, today=today) + ) == [due_1_5, due_1_6] - assert sorted([due_1_6, due_1_5, remind_1_5, remind_1_6], key=functools.partial(task_sorter, today=today)) == \ - [remind_1_5, due_1_5, due_1_6, remind_1_6] - assert sorted([remind_1_5, remind_1_6], key=functools.partial(task_sorter, today=today)) == \ - [remind_1_5, remind_1_6] + assert sorted( + [due_1_6, due_1_5, remind_1_5, remind_1_6], + key=functools.partial(task_sorter, today=today), + ) == [remind_1_5, due_1_5, due_1_6, remind_1_6] + assert sorted( + [remind_1_5, remind_1_6], key=functools.partial(task_sorter, today=today) + ) == [remind_1_5, remind_1_6] - assert sorted([due_1_7, due_1_5, remind_1_5, remind_1_6], key=functools.partial(task_sorter, today=today)) == \ - [remind_1_5, due_1_5, remind_1_6, due_1_7] + assert sorted( + [due_1_7, due_1_5, remind_1_5, remind_1_6], + key=functools.partial(task_sorter, today=today), + ) == [remind_1_5, due_1_5, remind_1_6, due_1_7] diff --git a/today-tasks.md b/today-tasks.md index 6f0d024..e412cbe 100644 --- a/today-tasks.md +++ b/today-tasks.md @@ -41,7 +41,7 @@ I'm generalizing this into a priority marking system. - [ ] Importance markers [d:3/12/2024] - [x] Rename date_defns to task_attrs - - [ ] Add importance markers parsing + - [x] Add importance markers parsing - [ ] Add new logic to tree printing - [ ] Add README docs diff --git a/today/parser.py b/today/parser.py index 5053d0e..81354e6 100644 --- a/today/parser.py +++ b/today/parser.py @@ -1,4 +1,4 @@ -from typing import Tuple, List, Optional +from typing import Sequence, Tuple, List, Optional from datetime import date import re from more_itertools import windowed @@ -144,7 +144,7 @@ def parse_task_title(title: str, today: date) -> Task: return t -def parse_markdown(md: List[str], today: date = date.today()) -> List[Task]: +def parse_markdown(md: Sequence[str], today: date = date.today()) -> List[Task]: headings_stack: List[str] = [] current_task: Optional[Task] = None tasks: List[Task] = [] @@ -177,14 +177,7 @@ def parse_markdown(md: List[str], today: date = date.today()) -> List[Task]: subtask.path = headings_stack.copy() subtask.done = subtask_status subtask.line_number = i + 1 - if not subtask.due_date and current_task.due_date: - subtask.due_date = current_task.due_date - if not subtask.reminder_date and current_task.reminder_date: - subtask.reminder_date = current_task.reminder_date - if not subtask.finished_date and current_task.finished_date: - subtask.finished_date = current_task.finished_date - if not subtask.created_date and current_task.created_date: - subtask.created_date = current_task.created_date + subtask.attrs.merge_attributes(current_task.attrs) current_task.subtasks.append(subtask) elif len(line) == 0 and current_task is None: continue diff --git a/today/task.py b/today/task.py index c603f27..dae898c 100644 --- a/today/task.py +++ b/today/task.py @@ -39,6 +39,18 @@ def is_visible(self, today: date, lookahead_days: int) -> bool: else: return False + # If this is a subtask and we have the attributes of the parent task, + # propagate the parent attributes into the subtask + def merge_attributes(self, parent_attrs: "DateAttribute") -> None: + if self.created_date is None: + self.created_date = parent_attrs.created_date + if self.due_date is None: + self.due_date = parent_attrs.due_date + if self.reminder_date is None: + self.reminder_date = parent_attrs.reminder_date + if self.finished_date is None: + self.finished_date = parent_attrs.finished_date + @dataclass class AssignmentAttribute: @@ -60,6 +72,10 @@ class TaskAttributes: def is_visible(self, today: date, lookahead_days: int) -> bool: raise NotImplementedError() + def merge_attributes(self, parent_attrs: "TaskAttributes") -> None: + # TODO: are there other attributes to merge (priority or assignment?) + self.date_attr.merge_attributes(parent_attrs.date_attr) + def date_relative_to_today(d: date, today: date, prefix: str = "") -> str: if d < today: @@ -90,31 +106,14 @@ class Task: file_path: Path = Path.cwd() line_number: int = 0 - def is_displayed(self, date_limit: date) -> bool: - if self.due_date and self.due_date <= date_limit and self.done is False: - return True - elif ( - self.reminder_date - and self.reminder_date <= date_limit - and self.done is False - ): - return True - elif self.subtasks and any( - [ - st.due_date and st.due_date <= date_limit and st.done is False - for st in self.subtasks - ] - ): - return True - elif self.subtasks and any( - [ - st.reminder_date and st.reminder_date <= date_limit and st.done is False - for st in self.subtasks - ] - ): - return True - else: - return False + # A task should be displayed if it has a reminder or due date that is today or has passed + # If a task is already done then it should not be displayed no matter what + def is_displayed(self, today: date, lookahead_days: int = 0) -> bool: + task_visible = self.attrs.date_attr.is_visible(today, lookahead_days) + subtasks_visible = any( + [t.is_displayed(today, lookahead_days) for t in self.subtasks] + ) + return (task_visible or subtasks_visible) and not self.done def summary(self, today: date) -> str: # Returns a Markdown string reminder_msg: Optional[str] = None From 532e08a4025bb195d69fb68dec275637d69a4af2 Mon Sep 17 00:00:00 2001 From: Vighnesh Iyer Date: Tue, 19 Mar 2024 10:07:45 -0700 Subject: [PATCH 06/11] more wip --- tests/test_task.py | 10 ++++++++-- today/task.py | 47 ++++++++++++++++++++++++---------------------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/tests/test_task.py b/tests/test_task.py index 341d79d..a2a4b3e 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -40,7 +40,10 @@ def test_task_summary(self) -> None: assert task.summary(today=date(2022, 1, 10)) == "[**Due 5 days ago**]" # Task with only reminder date - task = Task(reminder_date=date(2022, 1, 5), title="Task 1") + task = Task( + attrs=TaskAttributes(DateAttribute(reminder_date=date(2022, 1, 5))), + title="Task 1", + ) assert task.summary(today=date(2022, 1, 5)) == "[**Reminder today**]" assert task.summary(today=date(2022, 1, 4)) == "[Reminder in 1 day]" assert task.summary(today=date(2022, 1, 1)) == "[Reminder in 4 days]" @@ -49,7 +52,10 @@ def test_task_summary(self) -> None: # Task with due and reminder dates task = Task( - due_date=date(2022, 1, 5), reminder_date=date(2022, 1, 1), title="Task 1" + attrs=TaskAttributes( + DateAttribute(due_date=date(2022, 1, 5), reminder_date=date(2022, 1, 1)) + ), + title="Task 1", ) assert task.summary(today=date(2022, 1, 5)) == "[**Due today**]" assert ( diff --git a/today/task.py b/today/task.py index dae898c..9fd3dde 100644 --- a/today/task.py +++ b/today/task.py @@ -51,6 +51,30 @@ def merge_attributes(self, parent_attrs: "DateAttribute") -> None: if self.finished_date is None: self.finished_date = parent_attrs.finished_date + def summary(self, today: date) -> str: + reminder_msg: Optional[str] = None + due_msg: Optional[str] = None + if self.due_date: + due_msg = date_relative_to_today(self.due_date, today, prefix="Due ") + if self.reminder_date: + reminder_msg = date_relative_to_today( + self.reminder_date, today, prefix="Reminder " + ) + + if self.reminder_date and not self.due_date: # Reminder only + assert reminder_msg + return f"[{reminder_msg}]" + elif self.due_date and not self.reminder_date: # Due date only + assert due_msg + return f"[{due_msg}]" + else: # Both due and reminder dates + assert reminder_msg and due_msg + assert self.due_date and self.reminder_date + if self.due_date > today: # Only show reminder if task is not overdue + return f"[{reminder_msg}] [{due_msg}]" + else: + return f"[{due_msg}]" + @dataclass class AssignmentAttribute: @@ -116,28 +140,7 @@ def is_displayed(self, today: date, lookahead_days: int = 0) -> bool: return (task_visible or subtasks_visible) and not self.done def summary(self, today: date) -> str: # Returns a Markdown string - reminder_msg: Optional[str] = None - due_msg: Optional[str] = None - if self.due_date: - due_msg = date_relative_to_today(self.due_date, today, prefix="Due ") - if self.reminder_date: - reminder_msg = date_relative_to_today( - self.reminder_date, today, prefix="Reminder " - ) - - if self.reminder_date and not self.due_date: # Reminder only - assert reminder_msg - return f"[{reminder_msg}]" - elif self.due_date and not self.reminder_date: # Due date only - assert due_msg - return f"[{due_msg}]" - else: # Both due and reminder dates - assert reminder_msg and due_msg - assert self.due_date and self.reminder_date - if self.due_date > today: # Only show reminder if task is not overdue - return f"[{reminder_msg}] [{due_msg}]" - else: - return f"[{due_msg}]" + return self.attrs.date_attr.summary(today) def details(self, task_id: int, today: date) -> str: # Returns a Markdown string string = "" From 7d3a91bf739d323614357a86424a28747e05c48f Mon Sep 17 00:00:00 2001 From: Vighnesh Iyer Date: Tue, 19 Mar 2024 17:27:31 -0700 Subject: [PATCH 07/11] basic refactoring complete --- tests/test_task.py | 32 +++++++++++++-------- today-tasks.md | 1 + today/task.py | 69 +++++++++++++++++++++++++--------------------- 3 files changed, 58 insertions(+), 44 deletions(-) diff --git a/tests/test_task.py b/tests/test_task.py index a2a4b3e..50a17bc 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -75,18 +75,26 @@ def test_task_summary(self) -> None: def test_task_sorting(self) -> None: today = date(2022, 1, 6) - due_1_5 = Task(title="due_1_5", due_date=date(2022, 1, 5)) - remind_1_5 = Task(title="remind_1_5", reminder_date=date(2022, 1, 5)) - due_1_6 = Task(title="due_1_6", due_date=date(2022, 1, 6)) - remind_1_6 = Task(title="remind_1_6", reminder_date=date(2022, 1, 6)) - due_1_7 = Task(title="due_1_7", due_date=date(2022, 1, 7)) - - # sort by: - # 1. heading path - # 2. past due tasks - # 3. tasks due today - # 4. tasks with reminders today or in the past - # 5. tasks with due/reminder dates in the future + due_1_5 = Task( + title="due_1_5", + attrs=TaskAttributes(DateAttribute(due_date=date(2022, 1, 5))), + ) + remind_1_5 = Task( + title="remind_1_5", + attrs=TaskAttributes(DateAttribute(reminder_date=date(2022, 1, 5))), + ) + due_1_6 = Task( + title="due_1_6", + attrs=TaskAttributes(DateAttribute(due_date=date(2022, 1, 6))), + ) + remind_1_6 = Task( + title="remind_1_6", + attrs=TaskAttributes(DateAttribute(reminder_date=date(2022, 1, 6))), + ) + due_1_7 = Task( + title="due_1_7", + attrs=TaskAttributes(DateAttribute(due_date=date(2022, 1, 7))), + ) # Task due at 1/5 is past due vs the task due at 1/6 (due today) assert sorted( diff --git a/today-tasks.md b/today-tasks.md index e412cbe..1447951 100644 --- a/today-tasks.md +++ b/today-tasks.md @@ -42,6 +42,7 @@ I'm generalizing this into a priority marking system. - [ ] Importance markers [d:3/12/2024] - [x] Rename date_defns to task_attrs - [x] Add importance markers parsing + - [ ] Cleanup task printing (summary + description) logic into attributes - [ ] Add new logic to tree printing - [ ] Add README docs diff --git a/today/task.py b/today/task.py index 9fd3dde..aafa82c 100644 --- a/today/task.py +++ b/today/task.py @@ -4,18 +4,31 @@ from pathlib import Path +# Some functions to simplify stringifying task descriptions and summaries +def date_relative_to_today(d: date, today: date, prefix: str = "") -> str: + if d < today: + delta: timedelta = today - d + return f"**{prefix}{days(delta)} ago**" # Something already passed, high priority (bold) + elif d == today: + return f"**{prefix}today**" # Something today, high priority (bold) + else: + delta_inv: timedelta = d - today + return f"{prefix}in {days(delta_inv)}" # Something in the future, low priority + + +def days(days: timedelta) -> str: + if days.days == 1: + return f"{days.days} day" + else: + return f"{days.days} days" + + @dataclass class Heading: level: int name: str -@dataclass -class RawAttribute: - prefix: str - value: str - - TaskTitle = str @@ -75,6 +88,14 @@ def summary(self, today: date) -> str: else: return f"[{due_msg}]" + def details(self, today: date) -> str: + string = "" + if self.due_date: + string += f"**Due date**: {self.due_date} ({date_relative_to_today(self.due_date, today, prefix='Due ')}) \n" + if self.reminder_date: + string += f"**Reminder date**: {self.reminder_date} ({date_relative_to_today(self.reminder_date, today, prefix='Reminder ')}) \n" + return string + @dataclass class AssignmentAttribute: @@ -101,24 +122,6 @@ def merge_attributes(self, parent_attrs: "TaskAttributes") -> None: self.date_attr.merge_attributes(parent_attrs.date_attr) -def date_relative_to_today(d: date, today: date, prefix: str = "") -> str: - if d < today: - delta: timedelta = today - d - return f"**{prefix}{days(delta)} ago**" # Something already passed, high priority (bold) - elif d == today: - return f"**{prefix}today**" # Something today, high priority (bold) - else: - delta_inv: timedelta = d - today - return f"{prefix}in {days(delta_inv)}" # Something in the future, low priority - - -def days(days: timedelta) -> str: - if days.days == 1: - return f"{days.days} day" - else: - return f"{days.days} days" - - @dataclass class Task: path: List[str] = field(default_factory=lambda: []) @@ -145,25 +148,27 @@ def summary(self, today: date) -> str: # Returns a Markdown string def details(self, task_id: int, today: date) -> str: # Returns a Markdown string string = "" string += f"**Title**: {self.title} (id = `{task_id}`) \n" - if self.due_date: - string += f"**Due date**: {self.due_date} ({date_relative_to_today(self.due_date, today, prefix='Due ')}) \n" - if self.reminder_date: - string += f"**Reminder date**: {self.reminder_date} ({date_relative_to_today(self.reminder_date, today, prefix='Reminder ')}) \n" if len(self.description) > 0: string += "**Description**: \n\n" string += self.description return string +# sort by: +# 1. heading path +# 2. past due tasks +# 3. tasks due today +# 4. tasks with reminders today or in the past +# 5. tasks with due/reminder dates in the future def task_sorter(task: Task, today: date) -> Any: keys: List[Any] = [] keys.append(task.path) - if task.reminder_date: - keys.append(task.reminder_date - today) + if task.attrs.date_attr.reminder_date: + keys.append(task.attrs.date_attr.reminder_date - today) else: keys.append(timedelta(days=0)) - if task.due_date: - keys.append(task.due_date - today) + if task.attrs.date_attr.due_date: + keys.append(task.attrs.date_attr.due_date - today) else: keys.append(timedelta(days=0)) return keys From 9ffbf6cadf69cf4bf7a2f11b63e4bdbb5c6e28aa Mon Sep 17 00:00:00 2001 From: Vighnesh Iyer Date: Wed, 20 Mar 2024 16:40:40 -0700 Subject: [PATCH 08/11] work on displaying priorities --- example/chores.md | 2 +- today/cli.py | 42 +++++++++++++++++------------------------- today/scripts/today.py | 15 +++++++++++---- today/task.py | 13 +++++++++++-- 4 files changed, 40 insertions(+), 32 deletions(-) diff --git a/example/chores.md b/example/chores.md index 273a110..e31776f 100644 --- a/example/chores.md +++ b/example/chores.md @@ -3,7 +3,7 @@ ## Cleaning *italics* - [ ] Clean the *countertops* [d:1/11/2022] -- [ ] ***Wash my car*** [d:1/10/2022] +- [ ] ***Wash my car*** [d:1/10/2022] [!0] - [x] Sweep the floors [d:1/5/2022] [f:1/4/2022] - [ ] `Another task` [d:t] diff --git a/today/cli.py b/today/cli.py index 9b87f1c..9f171bd 100644 --- a/today/cli.py +++ b/today/cli.py @@ -112,34 +112,26 @@ def parse_task_files(args: CliArgs) -> List[Task]: return tasks_visible -def maybe_display_specific_task( - args: CliArgs, tasks: List[Task], console: Console +def display_specific_task( + task: Task, task_id: int, today: date, console: Console ) -> None: - # If a specific task id is given, print its description and details and exit - if args.task_id is not None: - if args.task_id < 0 or args.task_id >= len(tasks): - console.print(f"The task_id {args.task_id} does not exist") - sys.exit(1) - task = tasks[args.task_id] - details = task.details(args.task_id, args.today) - console.print("") - console.print(Markdown(details)) + details = task.details(task_id, today) + console.print("") + console.print(Markdown(details)) + console.print("") + + if len(task.subtasks) > 0: + console.print(Markdown("**Subtasks**:")) + for subtask in task.subtasks: + subtask_summary = subtask.summary(today) + if subtask.done: + console.print(Markdown(f"- **DONE**: {subtask.title} {subtask_summary}")) + else: + console.print(Markdown(f"- {subtask.title} {subtask_summary}")) + if len(task.subtasks) > 0: console.print("") - if len(task.subtasks) > 0: - console.print(Markdown("**Subtasks**:")) - for subtask in task.subtasks: - subtask_summary = subtask.summary(args.today) - if subtask.done: - console.print( - Markdown(f"- **DONE**: {subtask.title} {subtask_summary}") - ) - else: - console.print(Markdown(f"- {subtask.title} {subtask_summary}")) - if len(task.subtasks) > 0: - console.print("") - - sys.exit(0) + sys.exit(0) def tasks_to_tree(args: CliArgs, tasks: List[Task]) -> Tree: diff --git a/today/scripts/today.py b/today/scripts/today.py index 7be5810..31354da 100644 --- a/today/scripts/today.py +++ b/today/scripts/today.py @@ -6,7 +6,7 @@ build_parser, parse_args, parse_task_files, - maybe_display_specific_task, + display_specific_task, tasks_to_tree, ) @@ -16,10 +16,17 @@ def run(args) -> None: cli_args = parse_args(parser, args) console = Console() + # TODO: cache task parsing for each markdown file based on file hash tasks = parse_task_files(cli_args) - maybe_display_specific_task( - cli_args, tasks, console - ) # If a specific task is displayed, the program will exit + + # If a specific task is displayed, the program will exit + if cli_args.task_id is not None: + if cli_args.task_id < 0 or cli_args.task_id >= len(tasks): + console.print(f"The task_id {args.task_id} does not exist") + sys.exit(1) + task = tasks[cli_args.task_id] + display_specific_task(task, cli_args.task_id, cli_args.today, console) + sys.exit(0) tree = tasks_to_tree(cli_args, tasks) console.print("") diff --git a/today/task.py b/today/task.py index aafa82c..e85727d 100644 --- a/today/task.py +++ b/today/task.py @@ -107,6 +107,9 @@ class PriorityAttribute: # [priority] of 0 is higher than [priority] of 1 priority: int + def summary(self) -> str: + return f"[***Priority*** = {self.priority}]" + @dataclass class TaskAttributes: @@ -118,7 +121,7 @@ def is_visible(self, today: date, lookahead_days: int) -> bool: raise NotImplementedError() def merge_attributes(self, parent_attrs: "TaskAttributes") -> None: - # TODO: are there other attributes to merge (priority or assignment?) + # TODO: are there other attributes to merge? (priority or assignment) self.date_attr.merge_attributes(parent_attrs.date_attr) @@ -143,7 +146,13 @@ def is_displayed(self, today: date, lookahead_days: int = 0) -> bool: return (task_visible or subtasks_visible) and not self.done def summary(self, today: date) -> str: # Returns a Markdown string - return self.attrs.date_attr.summary(today) + date_summary = self.attrs.date_attr.summary(today) + pri_summary = ( + (self.attrs.priority_attr.summary() + " ") + if self.attrs.priority_attr + else "" + ) + return pri_summary + date_summary def details(self, task_id: int, today: date) -> str: # Returns a Markdown string string = "" From f6d0d48b999db7ff6372b4ac3b50435a3f78c6bb Mon Sep 17 00:00:00 2001 From: Vighnesh Iyer Date: Wed, 20 Mar 2024 17:44:17 -0700 Subject: [PATCH 09/11] implement priority task printing to tree --- tests/test_task.py | 19 ++++++++++++++++++- today-tasks.md | 2 +- today/cli.py | 18 ++++++++++++++---- today/task.py | 7 +++++++ 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/tests/test_task.py b/tests/test_task.py index 50a17bc..8793b15 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -3,7 +3,13 @@ import functools from dataclasses import replace -from today.task import DateAttribute, Task, TaskAttributes, task_sorter +from today.task import ( + DateAttribute, + PriorityAttribute, + Task, + TaskAttributes, + task_sorter, +) def remove_control_characters(s): @@ -95,6 +101,12 @@ def test_task_sorting(self) -> None: title="due_1_7", attrs=TaskAttributes(DateAttribute(due_date=date(2022, 1, 7))), ) + pri0_task = Task( + title="pri0", attrs=TaskAttributes(priority_attr=PriorityAttribute(0)) + ) + pri1_task = Task( + title="pri1", attrs=TaskAttributes(priority_attr=PriorityAttribute(1)) + ) # Task due at 1/5 is past due vs the task due at 1/6 (due today) assert sorted( @@ -116,3 +128,8 @@ def test_task_sorting(self) -> None: [due_1_7, due_1_5, remind_1_5, remind_1_6], key=functools.partial(task_sorter, today=today), ) == [remind_1_5, due_1_5, remind_1_6, due_1_7] + + assert sorted( + [due_1_7, due_1_5, pri1_task, pri0_task], + key=functools.partial(task_sorter, today=today), + ) == [pri0_task, pri1_task, due_1_5, due_1_7] diff --git a/today-tasks.md b/today-tasks.md index 1447951..324f45e 100644 --- a/today-tasks.md +++ b/today-tasks.md @@ -42,7 +42,7 @@ I'm generalizing this into a priority marking system. - [ ] Importance markers [d:3/12/2024] - [x] Rename date_defns to task_attrs - [x] Add importance markers parsing - - [ ] Cleanup task printing (summary + description) logic into attributes + - [x] Cleanup task printing (summary + description) logic into attributes - [ ] Add new logic to tree printing - [ ] Add README docs diff --git a/today/cli.py b/today/cli.py index 9f171bd..644730a 100644 --- a/today/cli.py +++ b/today/cli.py @@ -11,7 +11,7 @@ from rich.console import Console from rich.markdown import Markdown -from today.task import Task, task_sorter, days +from today.task import PriorityAttribute, Task, task_sorter, days from today.parser import parse_markdown @@ -107,7 +107,7 @@ def parse_task_files(args: CliArgs) -> List[Task]: task for task in tasks if task.is_displayed(args.task_date_filter()) ] - # Sort tasks by their headings and due dates + # Sort tasks by their priorities and headings and due dates tasks_visible.sort(key=functools.partial(task_sorter, today=args.today)) return tasks_visible @@ -145,6 +145,16 @@ def tasks_to_tree(args: CliArgs, tasks: List[Task]) -> Tree: ) ) + # Tasks should already be sorted with priority tasks first, then non-priority tasks + priority_tasks = [t for t in tasks if t.attrs.priority_attr is not None] + other_tasks = [t for t in tasks if t.attrs.priority_attr is None] + + priority_label = tree.add("[bold]Priority Tasks[/bold]") + for i, task in enumerate(priority_tasks): + priority_label.add( + f"[bold]{i}[/bold] - [blue]{' / '.join(task.path)}[/blue] [blue bold]➔[/blue bold] {task.title} ([red italic]{task.file_path.relative_to(args.task_dir)}:{task.line_number}[/red italic])" + ) + def add_to_tree(task: Task, tree: Tree, task_idx: int, first_call: bool) -> Tree: if len(task.path) == 0: # Base case parent = tree.add( @@ -179,6 +189,6 @@ def add_to_tree(task: Task, tree: Tree, task_idx: int, first_call: bool) -> Tree task.path = task.path[1:] return add_to_tree(task, child, task_idx, False) - for i, task in enumerate(tasks): - add_to_tree(task, tree, i, True) + for i, task in enumerate(other_tasks): + add_to_tree(task, tree, i + len(priority_tasks), True) return tree diff --git a/today/task.py b/today/task.py index e85727d..7b2b177 100644 --- a/today/task.py +++ b/today/task.py @@ -164,6 +164,7 @@ def details(self, task_id: int, today: date) -> str: # Returns a Markdown strin # sort by: +# 0. task priority # 1. heading path # 2. past due tasks # 3. tasks due today @@ -171,6 +172,12 @@ def details(self, task_id: int, today: date) -> str: # Returns a Markdown strin # 5. tasks with due/reminder dates in the future def task_sorter(task: Task, today: date) -> Any: keys: List[Any] = [] + if task.attrs.priority_attr: + keys.append(task.attrs.priority_attr.priority) + else: + keys.append( + 100000 + ) # very big number to not consider priority for tasks without priorities defined keys.append(task.path) if task.attrs.date_attr.reminder_date: keys.append(task.attrs.date_attr.reminder_date - today) From 46efd9280b9759ef6eeacf1d345e1c3117363bbd Mon Sep 17 00:00:00 2001 From: Vighnesh Iyer Date: Wed, 20 Mar 2024 18:48:18 -0700 Subject: [PATCH 10/11] add readme + some priority printing cleanup --- README.md | 42 ++++++++++++++++++++++++++++++++---------- today-tasks.md | 4 ++-- today/cli.py | 5 ++++- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 0236c1f..04e2035 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ The task files are the only source of truth! `today` is a read-only utility to display what's planned for today. To mark a task complete, edit `tasks.md` and tick its Markdown checkbox. -## Detailed Docs +## Docs ### Task Files @@ -62,16 +62,40 @@ Ideally each project should be tightly scoped and not drag on forever. - You can save Markdown files on disk in any way you want. Add nested folders to encode hierarchy. - You can group tasks within a task file any way you want. Add nested Markdown headings to encode hierarchy. -### Task Definitions +### Defining Tasks Tasks are defined with a list item that starts with a Markdown checkbox. +Mark a task as completed by checking its checkbox. -A task can have a created, reminder, due, and finished date by placing it in square brackets with the prefix `c:`, `r:`, `d:`, or `f:`. +```markdown +- [x] A completed task +- [ ] An example task +``` + +#### Task Description + +You can add a description for a task underneath the task title. +It can consist of any Markdown you want (*except headings*). -- The date is in `month`/`day`/`year` format -- `t` is a shorthand date for today. For example, if a task should be due today, use: `[d:t]` +#### Task Attributes -You can add a description for a task underneath the task title. It can consist of any Markdown you want (*except headings*). +Task attributes are written in the same line as the task title. + +- Date attributes + - Created date: `[c:]` + - Reminder date: `[r]` + - Due date: `[d:]` + - Finished date: `[f:]` + - Date is in `month`/`day`/`year` format + - `t` is a shorthand date for today + - For example, if a task should be due today, write `[d:t]` +- Assignment attribute + - `[@]` assigns a task to the given `username` +- Priority attribute + - `[!0]` sets the priority of a task to `0`. `0` is the highest priority, followed by `1`, `2`, ... + - Tasks without a priority attribute are printed separately from tasks marked with a priority + +#### Subtasks Subtasks are specified with a nested list of checkboxes under the main task. @@ -79,9 +103,7 @@ Subtasks are specified with a nested list of checkboxes under the main task. - If the main task has a created/reminder/due date, it will apply for all subtasks automatically, unless otherwise specified - Only one level of subtasks is supported -A task can be marked complete just by checking its checkbox. You can optionally specify a completion time with a finish `f` date. - -Here is a complete example: +### Example ```markdown - [x] Pay the electricity bill [d:t] [f:2/20/2023] @@ -90,7 +112,7 @@ Here is a complete example: - [ ] Wipe the countertops - [ ] Throw the trash -Any text here will be part of the main task's description. +Any text here will be part of the main task's ("Home Cleaning") description. - Some bullets - A nested bullet diff --git a/today-tasks.md b/today-tasks.md index 324f45e..6d99820 100644 --- a/today-tasks.md +++ b/today-tasks.md @@ -43,8 +43,8 @@ I'm generalizing this into a priority marking system. - [x] Rename date_defns to task_attrs - [x] Add importance markers parsing - [x] Cleanup task printing (summary + description) logic into attributes - - [ ] Add new logic to tree printing - - [ ] Add README docs + - [x] Add new logic to tree printing + - [x] Add README docs ### Assignees diff --git a/today/cli.py b/today/cli.py index 644730a..9336a2a 100644 --- a/today/cli.py +++ b/today/cli.py @@ -152,7 +152,10 @@ def tasks_to_tree(args: CliArgs, tasks: List[Task]) -> Tree: priority_label = tree.add("[bold]Priority Tasks[/bold]") for i, task in enumerate(priority_tasks): priority_label.add( - f"[bold]{i}[/bold] - [blue]{' / '.join(task.path)}[/blue] [blue bold]➔[/blue bold] {task.title} ([red italic]{task.file_path.relative_to(args.task_dir)}:{task.line_number}[/red italic])" + # f"[bold]{i}[/bold] - [blue]{' / '.join(task.path)}[/blue] [blue bold]➔[/blue bold] {task.title} {Markdown(task.summary(args.today))} ([red italic]{task.file_path.relative_to(args.task_dir)}:{task.line_number}[/red italic])" + Markdown( + f"**{i}** - {' / '.join(task.path)} → {task.title} {task.summary(args.today)} (*{task.file_path.relative_to(args.task_dir)}:{task.line_number}*)" + ) ) def add_to_tree(task: Task, tree: Tree, task_idx: int, first_call: bool) -> Tree: From 1dce8fc154bba2b8e6627b81b9413cf3e705173c Mon Sep 17 00:00:00 2001 From: Vighnesh Iyer Date: Wed, 20 Mar 2024 18:50:33 -0700 Subject: [PATCH 11/11] fix up union type + switch to pyright for ci --- .github/workflows/pr.yml | 4 ++-- today/parser.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index c05ecb5..78f0f00 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -17,7 +17,7 @@ jobs: continue-on-error: true outputs: pytestOutput: ${{ steps.unit-tests.outputs.test }} - mypyOutput: ${{ steps.type-checks.outputs.test }} + pyrightOutput: ${{ steps.type-checks.outputs.test }} strategy: matrix: python-version: ["3.9", "3.10", "3.11"] @@ -59,4 +59,4 @@ jobs: - name: Type checking id: type-checks run: | - poetry run mypy --namespace-packages -p today + poetry run pyright diff --git a/today/parser.py b/today/parser.py index 81354e6..14c8600 100644 --- a/today/parser.py +++ b/today/parser.py @@ -1,4 +1,4 @@ -from typing import Sequence, Tuple, List, Optional +from typing import Sequence, Tuple, List, Optional, Union from datetime import date import re from more_itertools import windowed @@ -60,7 +60,7 @@ def md_checkbox(s: str) -> Optional[bool]: # If the prefix or value are malformed, return an error message def assign_task_attr( prefix: str, value: str, task_attr: TaskAttributes, today: date -) -> None | str: +) -> Union[None, str]: if prefix == "@": # This is an assignment attribute task_attr.assn_attr = AssignmentAttribute(value)