From 911facc587bb8f5e295f679977d0e4c5d22fbbd2 Mon Sep 17 00:00:00 2001 From: jupblb Date: Sun, 28 Jun 2026 18:03:07 +0200 Subject: [PATCH 1/2] Update nixpkgs to 26.05 --- flake.lock | 14 +++++++------- flake.nix | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/flake.lock b/flake.lock index 25055df..8470318 100644 --- a/flake.lock +++ b/flake.lock @@ -20,16 +20,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1771875364, - "narHash": "sha256-g5+tYWFgkjKJhTMY8QtxBepcMKUS4d9X+/1+m5FHS8o=", + "lastModified": 1782660013, + "narHash": "sha256-SHXXOb0mPFNZbTOlsB0dFJnUEIdJJ0rC7VTWoEEvOrw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e80f56afd41881b9032301905f23808da359c1c9", + "rev": "0f42635fd2c1dd7e850ed88e0db52e80c130c3ef", "type": "github" }, "original": { "owner": "NixOS", - "ref": "release-25.11", + "ref": "release-26.05", "repo": "nixpkgs", "type": "github" } @@ -41,11 +41,11 @@ ] }, "locked": { - "lastModified": 1771518446, - "narHash": "sha256-nFJSfD89vWTu92KyuJWDoTQJuoDuddkJV3TlOl1cOic=", + "lastModified": 1782089418, + "narHash": "sha256-LRD1SuQWr49fGq3A+8GLXfsLE2xqIpQA440YDZwms3M=", "owner": "pyproject-nix", "repo": "pyproject.nix", - "rev": "eb204c6b3335698dec6c7fc1da0ebc3c6df05937", + "rev": "43f0b40edd0a74c63f66b7b48d969ae6b740d611", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 5f4f59a..acc9ea2 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,7 @@ { inputs = { flake-utils.url = "github:numtide/flake-utils"; - nixpkgs.url = "github:NixOS/nixpkgs/release-25.11"; + nixpkgs.url = "github:NixOS/nixpkgs/release-26.05"; pyproject-nix = { url = "github:pyproject-nix/pyproject.nix"; inputs.nixpkgs.follows = "nixpkgs"; From cc7d8015c65e0eddf5fafc38992a056acedc1cce Mon Sep 17 00:00:00 2001 From: jupblb Date: Sun, 28 Jun 2026 18:51:40 +0200 Subject: [PATCH 2/2] Migrate project to Python 3.13 --- .github/workflows/publish.yaml | 4 ++-- app/__main__.py | 26 ++++++++++++------------ app/backup.py | 19 +++++++++--------- app/editor.py | 19 +++++++++--------- app/googleapi.py | 30 +++++++++++++--------------- app/pandoc.py | 36 ++++++++++++++-------------------- app/tasks.py | 9 ++------- pyproject.toml | 2 +- 8 files changed, 64 insertions(+), 81 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index fd8012f..cc8da45 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -6,10 +6,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up Python 3.11 + - name: Set up Python 3.13 uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.13" cache: 'pip' - name: Install dependencies run: sudo apt-get install -y pandoc diff --git a/app/__main__.py b/app/__main__.py index d200b48..0d36f71 100644 --- a/app/__main__.py +++ b/app/__main__.py @@ -15,8 +15,8 @@ import asyncio import datetime import logging -import os from datetime import timedelta +from pathlib import Path from xdg import xdg_cache_home, xdg_data_home @@ -29,13 +29,13 @@ def main(): args = parse_args() - config_dir = f"{xdg_data_home()}/gtasks-md/{args.user}/" - os.makedirs(os.path.dirname(config_dir), exist_ok=True) - cache_dir = f"{xdg_cache_home()}/gtasks-md/{args.user}/" - os.makedirs(os.path.dirname(cache_dir), exist_ok=True) + config_dir = Path(xdg_data_home()) / "gtasks-md" / args.user + config_dir.mkdir(parents=True, exist_ok=True) + cache_dir = Path(xdg_cache_home()) / "gtasks-md" / args.user + cache_dir.mkdir(parents=True, exist_ok=True) logging.basicConfig( - filename=f"{xdg_cache_home()}/gtasks-md/log.txt", + filename=Path(xdg_cache_home()) / "gtasks-md" / "log.txt", format="%(asctime)s %(levelname)-8s %(message)s", encoding="utf-8", level=logging.INFO, @@ -137,8 +137,7 @@ def parse_date(date): def auth(service: GoogleApiService, file: str): - with open(file, "r") as src_file: - service.save_credentials(src_file.read()) + service.save_credentials(Path(file).read_text(encoding="utf-8")) def view(service: GoogleApiService): @@ -157,12 +156,11 @@ def edit(service: GoogleApiService, editor: Editor, backup: Backup): def reconcile(service: GoogleApiService, file_path: str, backup: Backup | None = None): old_task_lists, old_text = fetch_task_lists(service) - with open(file_path, "r") as source: - new_text = source.read() - new_task_lists = markdown_to_task_lists(new_text) - if backup: - backup.write_backup(old_text) - asyncio.run(service.reconcile(old_task_lists, new_task_lists)) + new_text = Path(file_path).read_text(encoding="utf-8") + new_task_lists = markdown_to_task_lists(new_text) + if backup: + backup.write_backup(old_text) + asyncio.run(service.reconcile(old_task_lists, new_task_lists)) def rollback(service: GoogleApiService, backup: Backup): diff --git a/app/backup.py b/app/backup.py index 2f8f2d5..a55ccd5 100644 --- a/app/backup.py +++ b/app/backup.py @@ -25,32 +25,31 @@ def __init__(self, user): self.user = user def write_backup(self, text: str): - cache_dir = f"{xdg_cache_home()}/gtasks-md/{self.user}" - marker_file_path = Path(f"{cache_dir}/marker") + cache_dir = Path(xdg_cache_home()) / "gtasks-md" / self.user + marker_file_path = cache_dir / "marker" marker_file_path.touch() - with marker_file_path.open("r+") as marker_file: + with marker_file_path.open("r+", encoding="utf-8") as marker_file: marker = marker_file.read() file_no = (int(marker) + 1 if marker else 0) % 10 marker_file.seek(0) marker_file.write(str(file_no)) marker_file.truncate() - with open(f"{cache_dir}/{file_no}.bak.md", "w") as backup_file: - backup_file.write(text) + (cache_dir / f"{file_no}.bak.md").write_text(text, encoding="utf-8") def discard_backup(self): - cache_dir = f"{xdg_cache_home()}/gtasks-md/{self.user}" - marker_file_path = Path(f"{cache_dir}/marker") + cache_dir = Path(xdg_cache_home()) / "gtasks-md" / self.user + marker_file_path = cache_dir / "marker" if not marker_file_path.is_file(): return None - with marker_file_path.open("r+") as marker_file: + with marker_file_path.open("r+", encoding="utf-8") as marker_file: marker = marker_file.read() - file_no = ((int(marker) if marker else 0)) % 10 + file_no = (int(marker) if marker else 0) % 10 marker_file.seek(0) marker_file.write(str(file_no - 1)) marker_file.truncate() - return f"{cache_dir}/{file_no}.bak.md" + return str(cache_dir / f"{file_no}.bak.md") diff --git a/app/editor.py b/app/editor.py index f5d73cf..3252e9c 100644 --- a/app/editor.py +++ b/app/editor.py @@ -14,6 +14,7 @@ import os import subprocess import tempfile +from pathlib import Path class Editor: @@ -41,17 +42,15 @@ def edit(self, text: str) -> str: program will try the following options: $VISUAL, $EDITOR and "vim". """ - tmp_file = "" - with tempfile.NamedTemporaryFile(suffix=".md", delete=False) as tmp: - tmp.write(str.encode(text)) + with tempfile.NamedTemporaryFile( + suffix=".md", + mode="w", + encoding="utf-8", + delete_on_close=False, + ) as tmp: + tmp.write(text) tmp.flush() if subprocess.call([self.editor, tmp.name]) != 0: exit(1) - tmp_file = tmp.name - out = "" - with open(tmp_file, "r") as output: - out = output.read() - - os.remove(tmp_file) - return out + return Path(tmp.name).read_text(encoding="utf-8") diff --git a/app/googleapi.py b/app/googleapi.py index 3b31a50..1cad4f7 100644 --- a/app/googleapi.py +++ b/app/googleapi.py @@ -13,10 +13,10 @@ # limitations under the License. import asyncio import logging -import os from collections import defaultdict from datetime import datetime from enum import Enum, auto +from pathlib import Path from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials @@ -243,9 +243,9 @@ def fix_task_order(task_list_id, new_tasks, parent_task_id=""): f" (parent: {parent_task_id})" ) - async_tasks = [] - for op in gen_tasklist_ops(): - async_tasks.append(asyncio.create_task(apply_task_list_op(op))) + async_tasks = [ + asyncio.create_task(apply_task_list_op(op)) for op in gen_tasklist_ops() + ] await asyncio.gather(*async_tasks) def fetch_task_lists(self) -> list[TaskList]: @@ -352,36 +352,34 @@ def get_credentials(self) -> Credentials: the process will simply fail. """ creds = None - config_dir = f"{xdg_data_home()}/gtasks-md/{self.user}" - credentials_file = f"{config_dir}/{CREDENTIALS_FILE}" - cache_dir = f"{xdg_cache_home()}/gtasks-md/{self.user}" - token_file = f"{cache_dir}/token.json" + config_dir = Path(xdg_data_home()) / "gtasks-md" / self.user + credentials_file = config_dir / CREDENTIALS_FILE + cache_dir = Path(xdg_cache_home()) / "gtasks-md" / self.user + token_file = cache_dir / "token.json" # The file token.json stores the user's access and refresh tokens, and is # created automatically when the authorization flow completes for the first # time. - if os.path.exists(token_file): - creds = Credentials.from_authorized_user_file(token_file, SCOPES) + if token_file.exists(): + creds = Credentials.from_authorized_user_file(str(token_file), SCOPES) # If there are no (valid) credentials available, let the user log in. if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) else: flow = InstalledAppFlow.from_client_secrets_file( - credentials_file, SCOPES + str(credentials_file), SCOPES ) creds = flow.run_local_server(port=0) # Save the credentials for the next run - with open(token_file, "w+") as token: - token.write(creds.to_json()) + token_file.write_text(creds.to_json(), encoding="utf-8") return creds def save_credentials(self, credentials: str): """Save credentials to selected user config directory.""" - config_dir = f"{xdg_data_home()}/gtasks-md/{self.user}" + config_dir = Path(xdg_data_home()) / "gtasks-md" / self.user - with open(f"{config_dir}/{CREDENTIALS_FILE}", "w+") as dest_file: - dest_file.write(credentials) + (config_dir / CREDENTIALS_FILE).write_text(credentials, encoding="utf-8") def _get_service(self): if not self._service: diff --git a/app/pandoc.py b/app/pandoc.py index 1ed8220..2c2bfba 100644 --- a/app/pandoc.py +++ b/app/pandoc.py @@ -39,22 +39,18 @@ def task_lists_to_markdown(task_lists: list[TaskList]) -> str: def text_to_pandoc(text: str): elems = [] for word in text.split(): - elems.append(Str(word)) - elems.append(Space()) + elems.extend((Str(word), Space())) return elems[:-1] def tasks_to_pandoc(tasks: list[Task]): - pandoc_tasks = [] has_notes = any(t.note for t in tasks) - for task in tasks: - pandoc_tasks.append(task_to_pandoc(task, has_notes)) - return pandoc_tasks + return [task_to_pandoc(task, has_notes) for task in tasks] def task_to_pandoc(task: Task, parent_contains_notes: bool): pandoc_task = [] task_sign = "☒" if task.completed() else "☐" - task_title = [Str(task_sign), Space()] + text_to_pandoc(task.title) + task_title = [Str(task_sign), Space(), *text_to_pandoc(task.title)] if parent_contains_notes: pandoc_task.append(Para(task_title)) @@ -69,10 +65,11 @@ def task_to_pandoc(task: Task, parent_contains_notes: bool): pandoc_task.append(Plain(task_title)) if task.subtasks: - subtasks = [] parent_contains_notes = any(st.note for st in task.subtasks) - for subtask in task.subtasks: - subtasks.append(task_to_pandoc(subtask, parent_contains_notes)) + subtasks = [ + task_to_pandoc(subtask, parent_contains_notes) + for subtask in task.subtasks + ] pandoc_task.append(OrderedList(ORDERED_FIRST_ELEM, subtasks)) @@ -83,9 +80,11 @@ def task_to_pandoc(task: Task, parent_contains_notes: bool): ] for task_list in task_lists: - content.append(Header(2, EMPTY_ATTRS, text_to_pandoc(task_list.title))) - content.append( - OrderedList(ORDERED_FIRST_ELEM, tasks_to_pandoc(task_list.tasks)) + content.extend( + ( + Header(2, EMPTY_ATTRS, text_to_pandoc(task_list.title)), + OrderedList(ORDERED_FIRST_ELEM, tasks_to_pandoc(task_list.tasks)), + ) ) return pandoc.write(Pandoc(Meta({}), content)) @@ -108,21 +107,16 @@ def parse_task_lists(items, idx): match items[idx + 1]: case OrderedList(_, tasks): task_list.tasks = parse_tasks(tasks) - return [task_list] + parse_task_lists(items, idx + 2) + return [task_list, *parse_task_lists(items, idx + 2)] case _: - return [task_list] + parse_task_lists(items, idx + 1) + return [task_list, *parse_task_lists(items, idx + 1)] else: return [task_list] case _: raise SyntaxError(f"Unexpected item while parsing: {items[idx]}") def parse_tasks(tasks): - parsed_tasks = [] - - for i, task in enumerate(tasks): - parsed_tasks.append(parse_task(task, i)) - - return parsed_tasks + return [parse_task(task, i) for i, task in enumerate(tasks)] def parse_task(task, task_no): def match_status(str: Str) -> TaskStatus: diff --git a/app/tasks.py b/app/tasks.py index 47db91a..88774be 100644 --- a/app/tasks.py +++ b/app/tasks.py @@ -47,8 +47,7 @@ def __eq__(self, other: Task) -> bool: self.title == other.title and self.note == other.note and self.status == other.status - and len(self.subtasks) == len(other.subtasks) - and not any(st != ot for (st, ot) in zip(self.subtasks, other.subtasks)) + and self.subtasks == other.subtasks ) def __str__(self) -> str: @@ -80,11 +79,7 @@ class TaskList: tasks: list[Task] def __eq__(self, other: TaskList) -> bool: - return ( - self.title == other.title - and len(self.tasks) == len(other.tasks) - and not any(st != ot for (st, ot) in zip(self.tasks, other.tasks)) - ) + return self.title == other.title and self.tasks == other.tasks def __str__(self) -> str: return f"{self.title} ({self.id}): {len(self.tasks)} tasks" diff --git a/pyproject.toml b/pyproject.toml index 8761803..5a559f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "0.0.10" description = "A tool to manage Google Tasks using a markdown document." readme = "README.md" license = "Apache-2.0" -requires-python = ">=3.11" +requires-python = ">=3.13" authors = [ { name = "Michal Kielbowicz", email = "gtasks-md@kielbowi.cz" }, ]