Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@
let
pkgs = (import nixpkgs) { inherit system; };
pythonEnv = pkgs.python3.withPackages (
project.renderers.withPackages {
python = pkgs.python3;
}
ps: (project.renderers.withPackages { python = pkgs.python3; } ps) ++ [ ps.mypy ]
);
in
{
Expand All @@ -52,6 +50,12 @@
touch $out
'';

mypy = pkgs.runCommand "mypy" { buildInputs = [ pythonEnv ]; } ''
cd ${self}
MYPY_CACHE_DIR="$TMPDIR/mypy-cache" mypy gtasks_md tests
touch $out
'';

nixfmt = pkgs.runCommand "nixfmt" { buildInputs = [ pkgs.nixfmt ]; } ''
find ${self} -name '*.nix' -exec nixfmt --check {} +
touch $out
Expand Down
5 changes: 3 additions & 2 deletions gtasks_md/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from .editor import Editor
from .googleapi import GoogleApiService
from .parser import markdown_to_task_lists, task_lists_to_markdown
from .tasks import TaskStatus


def main():
Expand Down Expand Up @@ -89,9 +90,9 @@ def parse_date(date):
parser.add_argument(
"--status",
dest="status",
default="",
default=None,
help="Task status. One of: needsAction, completed.",
type=str.lower,
type=TaskStatus,
)
parser.add_argument(
"--user",
Expand Down
28 changes: 21 additions & 7 deletions gtasks_md/googleapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from datetime import datetime
from enum import Enum, auto
from pathlib import Path
from typing import Literal

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
Expand All @@ -40,13 +41,13 @@ def __init__(
user: str,
completed_after: datetime | None,
completed_before: datetime | None,
task_status: TaskStatus,
task_status: TaskStatus | None,
):
self.user = user
self.completed_after = completed_after
self.completed_before = completed_before
self.task_status = TaskStatus(task_status) if task_status else None
self._credentials = None
self.task_status = task_status
self._credentials: Credentials | None = None
self._credentials_lock = threading.Lock()
self._local = threading.local()

Expand Down Expand Up @@ -76,7 +77,7 @@ async def reconcile(
"""

def gen_tasklist_ops():
task_list_to_op = {}
task_list_to_op: dict[str, TaskListOp] = {}
for task_list in old_task_lists:
task_list_to_op[task_list.title] = (ReconcileOp.DELETE, task_list)

Expand Down Expand Up @@ -200,7 +201,7 @@ def callback(request_id, response, exception):
return new_tasks

def gen_task_ops(old_tasks: list[Task], new_tasks: list[Task]):
task_to_op = {}
task_to_op: dict[str, TaskOp] = {}
for task in old_tasks:
task_to_op[task.title] = (ReconcileOp.DELETE, task)

Expand Down Expand Up @@ -252,8 +253,8 @@ def fetch_task_lists(self) -> list[TaskList]:
tasks for these task lists that are either completed at most 30 days ago
or are still pending completion.
"""
id_to_task_list = {}
task_id_to_subtasks = defaultdict(list)
id_to_task_list: dict[str, TaskList] = {}
task_id_to_subtasks: defaultdict[str, list[Task]] = defaultdict(list)

def create_request_with_callback(task_list_id, completed):
def fetch_tasks_request(task_list_id, completed, next_page_token=""):
Expand Down Expand Up @@ -406,3 +407,16 @@ class ReconcileOp(Enum):
INSERT = auto()
DELETE = auto()
UPDATE = auto()


type TaskListOp = (
tuple[Literal[ReconcileOp.INSERT], TaskList]
| tuple[Literal[ReconcileOp.DELETE], TaskList]
| tuple[Literal[ReconcileOp.UPDATE], TaskList, TaskList] # (op, old, new)
)

type TaskOp = (
tuple[Literal[ReconcileOp.INSERT], Task, int] # (op, task, idx)
| tuple[Literal[ReconcileOp.DELETE], Task]
| tuple[Literal[ReconcileOp.UPDATE], Task, Task, int] # (op, old, new, idx)
)
4 changes: 3 additions & 1 deletion gtasks_md/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,7 @@ def _blocks_source(blocks: list[SyntaxTreeNode], lines: list[str]) -> str:

if not blocks:
return ""
start, end = blocks[0].map[0], blocks[-1].map[1]
start_map, end_map = blocks[0].map, blocks[-1].map
assert start_map and end_map # block-level nodes always carry a source map
start, end = start_map[0], end_map[1]
return textwrap.dedent("\n".join(lines[start:end])).strip()
17 changes: 14 additions & 3 deletions gtasks_md/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ class TaskStatus(StrEnum):
COMPLETED = "completed"

@classmethod
def _missing_(cls, value: str):
def _missing_(cls, value: object) -> TaskStatus | None:
if not isinstance(value, str):
return None
for member in cls:
if member.value.casefold() == value.casefold():
return member
Expand All @@ -42,7 +44,12 @@ class Task:
status: TaskStatus
subtasks: list[Task]

def __eq__(self, other: Task) -> bool:
def __eq__(self, other: object) -> bool:
"""Compares Task contents, deliberately ignoring server-assigned
state (id, position) so that reconcile can match freshly parsed
tasks against fetched ones."""
if not isinstance(other, Task):
return NotImplemented
return (
self.title == other.title
and self.note == other.note
Expand Down Expand Up @@ -78,7 +85,11 @@ class TaskList:
title: str
tasks: list[Task]

def __eq__(self, other: TaskList) -> bool:
def __eq__(self, other: object) -> bool:
"""Compares TaskList contents, deliberately ignoring the
server-assigned id. See Task.__eq__."""
if not isinstance(other, TaskList):
return NotImplemented
return self.title == other.title and self.tasks == other.tasks

def __str__(self) -> str:
Expand Down
15 changes: 15 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,18 @@ gtasks-md = "gtasks_md.__main__:main"

[tool.ruff.lint]
extend-select = ["I", "N"]

[tool.mypy]
python_version = "3.13"
check_untyped_defs = true
warn_redundant_casts = true
warn_unused_ignores = true

[[tool.mypy.overrides]]
module = [
"google.*",
"google_auth_oauthlib.*",
"googleapiclient.*",
"xdg",
]
ignore_missing_imports = true
2 changes: 1 addition & 1 deletion tests/test_googleapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ class FakeGoogleApiService(GoogleApiService):
"""GoogleApiService whose API surface is backed by a FakeServer."""

def __init__(self, server: FakeServer):
super().__init__("test", None, None, "")
super().__init__("test", None, None, None)
self._server = server

def tasks(self):
Expand Down
Loading