diff --git a/pyproject.toml b/pyproject.toml index fb7409b..90bb3c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -191,7 +191,6 @@ exclude = [] select = ["ALL"] ignore = [ # remove them in phases - "A002", "ANN001", "ANN002", "ANN003", @@ -201,17 +200,8 @@ ignore = [ "ANN202", "ANN204", "ANN206", - "ANN401", "ARG001", "ARG002", - "B007", - "B008", - "B012", - "B904", - "C405", - "C408", - "C419", - "C901", "D100", "D100", "D101", @@ -220,8 +210,6 @@ ignore = [ "D102", "D103", "D103", - "D104", - "D104", "D105", "D107", "D107", @@ -232,54 +220,32 @@ ignore = [ "D401", "D415", "DTZ004", - "DTZ006", "E501", "E712", "EM101", "EM102", - "ERA001", - "EXE002", - "F403", - "F841", "FA100", "FA102", "FBT001", "FBT002", - "FIX002", "INP001", "N802", "N803", "N806", "PGH003", - "PLR0912", "PLR0913", "PLR2004", "PT009", - "PT018", - "PTH118", - "PTH123", - "RET503", "RET504", "RET505", "RUF012", "S101", "S101", - "S301", - "S506", - "S602", - "SIM108", - "SIM110", "SIM118", "SLF001", - "T100", - "T201", - "TCH001", - "TCH002", "TD002", "TD003", - "TD004", "TRY003", - "TRY004", "UP007", ] exclude = [] diff --git a/syncall/aggregator.py b/syncall/aggregator.py index 58c71ff..9d85644 100644 --- a/syncall/aggregator.py +++ b/syncall/aggregator.py @@ -39,9 +39,9 @@ def __init__( side_B: SyncSide, converter_B_to_A: ConverterFn, converter_A_to_B: ConverterFn, - resolution_strategy: ResolutionStrategy = AlwaysSecondRS(), + resolution_strategy: ResolutionStrategy | None = None, config_fname: Optional[str] = None, - ignore_keys: tuple[Sequence[str], Sequence[str]] = tuple(), + ignore_keys: tuple[Sequence[str], Sequence[str]] = (), catch_exceptions: bool = True, ): # Preferences manager @@ -58,6 +58,9 @@ def __init__( else: logger.debug(f"Using a custom configuration file ... -> {config_fname}") + if resolution_strategy is None: + resolution_strategy = AlwaysSecondRS() + self.prefs_manager = PrefsManager(app_name=app_name(), config_fname=config_fname) # Own config diff --git a/syncall/app_utils.py b/syncall/app_utils.py index 3d5265e..3fce71d 100644 --- a/syncall/app_utils.py +++ b/syncall/app_utils.py @@ -14,7 +14,7 @@ import sys from datetime import datetime from pathlib import Path -from typing import Any, Iterable, Mapping, NoReturn, Optional, Sequence, cast +from typing import TYPE_CHECKING, Any, Iterable, Mapping, NoReturn, Optional, Sequence, cast from urllib.parse import quote from bubop import ( @@ -38,7 +38,10 @@ ) from syncall.constants import COMBINATION_FLAGS, ISSUES_URL -from syncall.sync_side import SyncSide + +if TYPE_CHECKING: + from syncall.sync_side import SyncSide + from syncall.types import SupportsStr # Various resolution strategies with their respective names so that the user can choose which # one they want. ------------------------------------------------------------------------------ @@ -126,7 +129,7 @@ def get_config_name_for_args(*args) -> str: def quote_(obj: str) -> str: return quote(obj, safe="+,") - def format_(obj: Any) -> str: + def format_(obj: SupportsStr) -> str: if isinstance(obj, str): return quote_(obj) elif isinstance(obj, Iterable): @@ -385,6 +388,8 @@ def teardown(): if inform_about_config: inform_about_combination_name_usage(combination_name) + return 0 + if pdb_on_error: logger.warning( "pdb_on_error is enabled. Disabling exit hooks / not taking actions at the end " diff --git a/syncall/asana/__init__.py b/syncall/asana/__init__.py index e69de29..b70e928 100644 --- a/syncall/asana/__init__.py +++ b/syncall/asana/__init__.py @@ -0,0 +1 @@ +"""Asana side subpackage.""" diff --git a/syncall/asana/asana_side.py b/syncall/asana/asana_side.py index 0707126..f331c39 100644 --- a/syncall/asana/asana_side.py +++ b/syncall/asana/asana_side.py @@ -85,8 +85,6 @@ def update_item(self, item_id: AsanaGID, **changes): # - If the remote Asana task 'due_on' field is empty, update 'due_at'. # - If the remote Asana task 'due_on' field is not empty and the # 'due_at' field is empty, update 'due_on'. - # TODO: find a way to store this information locally, so we don't have - # to fetch the task from Asana to determine this. remote_task = self.get_item(item_id) if remote_task.get("due_on", None) is None: raw_task.pop("due_on", None) @@ -155,7 +153,6 @@ def items_are_identical( compare_keys.remove(key) # Special handling for 'due_at' and 'due_on' - # TODO: reduce ['due_at','due_on'] to 'due_at', compare and remove both # keys. if item1.get("due_at", None) is not None and item2.get("due_at", None) is not None: compare_keys.remove("due_on") diff --git a/syncall/asana/asana_task.py b/syncall/asana/asana_task.py index e6b22a5..5b37ed1 100644 --- a/syncall/asana/asana_task.py +++ b/syncall/asana/asana_task.py @@ -29,7 +29,7 @@ class AsanaTask(Mapping): "modified_at", } - def __getitem__(self, key) -> Any: + def __getitem__(self, key) -> Any: # noqa: ANN401 return getattr(self, key) def __iter__(self): diff --git a/syncall/caldav/caldav_side.py b/syncall/caldav/caldav_side.py index 40ed0f2..d5a81f2 100644 --- a/syncall/caldav/caldav_side.py +++ b/syncall/caldav/caldav_side.py @@ -1,10 +1,14 @@ -from typing import Any, Optional, Sequence +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Optional, Sequence -import caldav from bubop import logger from caldav.lib.error import NotFoundError from icalendar.prop import vCategory, vDatetime, vText -from item_synchronizer.types import ID + +if TYPE_CHECKING: + import caldav + from item_synchronizer.types import ID from syncall.app_utils import error_and_exit from syncall.caldav.caldav_utils import calendar_todos, icalendar_component, map_ics_to_item @@ -90,11 +94,13 @@ def _find_todo_by_id_raw(self, item_id: ID) -> Optional[caldav.CalendarObjectRes return item - def _find_todo_by_id(self, item_id: ID) -> Optional[dict]: + def _find_todo_by_id(self, item_id: ID) -> dict | None: raw_item = self._find_todo_by_id_raw(item_id=item_id) if raw_item: return map_ics_to_item(icalendar_component(raw_item)) + return None + def delete_single_item(self, item_id: ID): todo = self._find_todo_by_id_raw(item_id=item_id) if todo is not None: @@ -110,7 +116,7 @@ def update_item(self, item_id: ID, **changes): logger.opt(lazy=True).debug(f"Can't update item {item_id}\n\nchanges: {changes}") return - def set_(key: str, val: Any): + def set_(key: str, val: Any): # noqa: ANN401 icalendar_component(todo)[key] = val # pop the key:value (s) that we're intending to potentially update diff --git a/syncall/caldav/caldav_utils.py b/syncall/caldav/caldav_utils.py index d089819..4c80a8d 100644 --- a/syncall/caldav/caldav_utils.py +++ b/syncall/caldav/caldav_utils.py @@ -1,13 +1,15 @@ from __future__ import annotations import traceback -from typing import Optional, Sequence +from typing import TYPE_CHECKING, Optional, Sequence from uuid import UUID -import caldav from bubop import logger -from icalendar.prop import vCategory -from item_synchronizer.resolution_strategy import Item + +if TYPE_CHECKING: + import caldav + from icalendar.prop import vCategory + from item_synchronizer.resolution_strategy import Item def icalendar_component(obj: caldav.CalendarObjectResource): @@ -55,13 +57,13 @@ def _convert_one(name: str) -> str: # return a List[vCategory], each vCategory with a single name # Option 1: # - # CATEGORIES:bugwarrior - # CATEGORIES:github_working_on_it - # CATEGORIES:programming - # CATEGORIES:remindme + # | CATEGORIES:bugwarrior + # | CATEGORIES:github_working_on_it + # | CATEGORIES:programming + # | CATEGORIES:remindme # # Option 2: - # CATEGORIES:bugwarrior,github_bug,github_help_wanted,github_tw_gcal_sync,pro + # | CATEGORIES:bugwarrior,github_bug,github_help_wanted,github_tw_gcal_sync,pro all_categories = [] if isinstance(vcategories, Sequence): for vcategory in vcategories: @@ -91,19 +93,19 @@ def parse_caldav_item_desc( lines = [line.strip() for line in caldav_desc.split("\n") if line][1:] # annotations - i = 0 - for i, line in enumerate(lines): + _i = 0 + for _i, line in enumerate(lines): parts = line.split(":", maxsplit=1) if len(parts) == 2 and parts[0].lower().startswith("* annotation"): annotations.append(parts[1].strip()) else: break - if i == len(lines): + if _i == len(lines): return annotations, uuid # Iterate through rest of lines, find only the uuid - for line in lines[i:]: + for line in lines[_i:]: parts = line.split(":", maxsplit=1) if len(parts) == 2 and parts[0].lower().startswith("* uuid"): try: diff --git a/syncall/concrete_item.py b/syncall/concrete_item.py index 278b8c9..0ab96be 100644 --- a/syncall/concrete_item.py +++ b/syncall/concrete_item.py @@ -45,7 +45,7 @@ def id(self) -> Optional[ID]: def _id(self) -> Optional[str]: pass - def __getitem__(self, key: str) -> Any: + def __getitem__(self, key: str) -> Any: # noqa: ANN401 return getattr(self, key) def __iter__(self) -> Iterator[str]: diff --git a/syncall/filesystem/filesystem_file.py b/syncall/filesystem/filesystem_file.py index 4521cac..0004378 100644 --- a/syncall/filesystem/filesystem_file.py +++ b/syncall/filesystem/filesystem_file.py @@ -179,11 +179,14 @@ def title(self, new_title): @property def last_modified_date(self) -> datetime.datetime: - # TODO Amend this. + tzinfo = datetime.datetime.now().astimezone().tzinfo try: - return datetime.datetime.fromtimestamp(self._path.stat().st_mtime) + return datetime.datetime.fromtimestamp( + self._path.stat().st_mtime, + tz=tzinfo, + ) except FileNotFoundError: - return datetime.datetime.utcfromtimestamp(0) + return datetime.datetime.fromtimestamp(0, tz=tzinfo) def delete(self) -> None: """Mark this file for deletion.""" diff --git a/syncall/google/__init__.py b/syncall/google/__init__.py index e69de29..b64945a 100644 --- a/syncall/google/__init__.py +++ b/syncall/google/__init__.py @@ -0,0 +1 @@ +"""Google-related subpackage.""" diff --git a/syncall/google/gcal_side.py b/syncall/google/gcal_side.py index ea1a2e0..76b0a81 100644 --- a/syncall/google/gcal_side.py +++ b/syncall/google/gcal_side.py @@ -1,7 +1,8 @@ +from __future__ import annotations + import datetime -import os from pathlib import Path -from typing import Literal, Optional, Sequence, Union, cast +from typing import TYPE_CHECKING, Literal, Optional, Sequence, cast import dateutil import pkg_resources @@ -12,9 +13,12 @@ from syncall.google.google_side import GoogleSide from syncall.sync_side import SyncSide +if TYPE_CHECKING: + from syncall.types import GoogleDateT + DEFAULT_CLIENT_SECRET = pkg_resources.resource_filename( "syncall", - os.path.join("res", "gcal_client_secret.json"), + "res/gcal_client_secret.json", ) @@ -41,7 +45,7 @@ def __init__( self, *, calendar_summary="TaskWarrior Reminders", - client_secret, + client_secret: str | None, **kargs, ): if client_secret is None: @@ -52,7 +56,7 @@ def __init__( fullname="Google Calendar", scopes=["https://www.googleapis.com/auth/calendar"], credentials_cache=Path.home() / ".gcal_credentials.pickle", - client_secret=Path(client_secret), + client_secret=client_secret, **kargs, ) @@ -150,8 +154,8 @@ def _get_item_refresh(self, item_id: str) -> Optional[dict]: self._items_cache[item_id] = ret except HttpError: pass - finally: - return ret + + return ret def update_item(self, item_id, **changes): # Check if item is there @@ -218,7 +222,7 @@ def format_datetime(dt: datetime.datetime) -> str: return format_datetime_tz(dt) @classmethod - def parse_datetime(cls, dt: Union[str, dict, datetime.datetime]) -> datetime.datetime: + def parse_datetime(cls, dt: GoogleDateT) -> datetime.datetime: """Parse datetime given in the GCal format(s): - string with ('T', 'Z' separators). - (dateTime, dateZone) dictionary @@ -238,7 +242,7 @@ def parse_datetime(cls, dt: Union[str, dict, datetime.datetime]) -> datetime.dat elif isinstance(dt, datetime.datetime): return assume_local_tz_if_none(dt) else: - raise RuntimeError( + raise TypeError( f"Unexpected type of a given date item, type: {type(dt)}, contents: {dt}", ) diff --git a/syncall/google/gkeep_note_side.py b/syncall/google/gkeep_note_side.py index 14b696b..30fc376 100644 --- a/syncall/google/gkeep_note_side.py +++ b/syncall/google/gkeep_note_side.py @@ -1,12 +1,16 @@ from __future__ import annotations -from typing import Optional, Sequence +from typing import TYPE_CHECKING, Sequence from gkeepapi.node import Label, Note, TopLevelNode -from item_synchronizer.types import ID + +if TYPE_CHECKING: + from item_synchronizer.types import ID + + from syncall.concrete_item import ConcreteItem + from loguru import logger -from syncall.concrete_item import ConcreteItem from syncall.google.gkeep_note import GKeepNote from syncall.google.gkeep_side import GKeepSide @@ -28,8 +32,8 @@ def last_modification_key(cls) -> str: def __init__( self, - gkeep_labels: Sequence[str] = tuple(), - gkeep_ignore_labels: Sequence[str] = tuple(), + gkeep_labels: Sequence[str] = (), + gkeep_ignore_labels: Sequence[str] = (), **kargs, ) -> None: super().__init__(name="GKeep", fullname="Google Keep Notes", **kargs) @@ -41,7 +45,6 @@ def __init__( def start(self): super().start() - # TODO Test this # Label management -------------------------------------------------------------------- # Create given labels if they don't already exist, # Get the concrete classes from strings @@ -81,11 +84,13 @@ def node_is_of_type_note(node: TopLevelNode) -> bool: return tuple(GKeepNote.from_gkeep_note(m) for m in matching) - def get_item(self, item_id: str, use_cached: bool = True) -> Optional[GKeepNote]: + def get_item(self, item_id: str, use_cached: bool = True) -> GKeepNote | None: for item in self.get_all_items(): if item.id == item_id: return item + return None + def _get_item_by_id(self, item_id: ID) -> GKeepNote: item = self.get_item(item_id=item_id) if item is None: diff --git a/syncall/google/gkeep_side.py b/syncall/google/gkeep_side.py index 71a142d..e8bf472 100644 --- a/syncall/google/gkeep_side.py +++ b/syncall/google/gkeep_side.py @@ -54,25 +54,19 @@ def finish(self): def _note_has_label(self, note: TopLevelNode, label: Label) -> bool: """True if the given Google Keep note has the given label.""" - for la in note.labels.all(): - if label == la: - return True - - return False + return any(label == la for la in note.labels.all()) def _note_has_label_str(self, note: TopLevelNode, label_str: str) -> bool: """True if the given Google Keep note has the given label.""" - for la in note.labels.all(): - if label_str == la.name: - return True - - return False + return any(label_str == la.name for la in note.labels.all()) def _get_label_by_name(self, label: str) -> Optional[Label]: for la in self._keep.labels(): if la.name == label: return la + return None + def _create_list(self, title: str, label: Optional[Label] = None) -> GKeepList: """Create a new list of items in Google Keep. diff --git a/syncall/google/google_side.py b/syncall/google/google_side.py index 191b4dd..8502d1c 100644 --- a/syncall/google/google_side.py +++ b/syncall/google/google_side.py @@ -17,7 +17,7 @@ def __init__( scopes: Sequence[str], oauth_port: int, credentials_cache: Path, - client_secret: Path, + client_secret: str, **kargs, ): super().__init__(**kargs) @@ -42,15 +42,17 @@ def _get_credentials(self): credentials_cache = self._credentials_cache if credentials_cache.is_file(): with credentials_cache.open("rb") as f: - creds = pickle.load(f) + creds = pickle.load(f) # noqa: S301 if not creds or not creds.valid: logger.debug("Invalid credentials. Fetching again...") if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) else: - client_secret = self._client_secret - flow = InstalledAppFlow.from_client_secrets_file(client_secret, self._scopes) + flow = InstalledAppFlow.from_client_secrets_file( + self._client_secret, + self._scopes, + ) try: creds = flow.run_local_server(port=self._oauth_port) except OSError as e: diff --git a/syncall/google/gtasks_side.py b/syncall/google/gtasks_side.py index 3d456fe..677fd0e 100644 --- a/syncall/google/gtasks_side.py +++ b/syncall/google/gtasks_side.py @@ -1,9 +1,8 @@ from __future__ import annotations import datetime -import os from pathlib import Path -from typing import Optional, Sequence, Union, cast +from typing import TYPE_CHECKING, Optional, Sequence, Union, cast import dateutil import pkg_resources @@ -14,11 +13,13 @@ from syncall.google.google_side import GoogleSide from syncall.sync_side import SyncSide -from syncall.types import GTasksItem, GTasksList + +if TYPE_CHECKING: + from syncall.types import GTasksItem, GTasksList DEFAULT_CLIENT_SECRET = pkg_resources.resource_filename( "syncall", - os.path.join("res", "gtasks_client_secret.json"), + "res/gtasks_client_secret.json", ) # API Reference: https://googleapis.github.io/google-api-python-client/docs/dyn/tasks_v1.html @@ -50,7 +51,7 @@ def __init__( self, *, task_list_title="TaskWarrior Reminders", - client_secret, + client_secret: str | None, **kargs, ): if client_secret is None: @@ -61,7 +62,7 @@ def __init__( fullname="Google Tasks", scopes=["https://www.googleapis.com/auth/tasks"], credentials_cache=Path.home() / ".gtasks_credentials.pickle", - client_secret=client_secret, # type: ignore + client_secret=client_secret, **kargs, ) @@ -187,8 +188,8 @@ def _get_item_refresh(self, item_id: str) -> Optional[dict]: self._items_cache[item_id] = ret except HttpError: pass - finally: - return ret + + return ret def update_item(self, item_id, **changes): # Check if item is there @@ -295,7 +296,7 @@ def parse_datetime(cls, dt: Union[str, dict, datetime.datetime]) -> datetime.dat elif isinstance(dt, datetime.datetime): return dt else: - raise RuntimeError( + raise TypeError( f"Unexpected type of a given date item, type: {type(dt)}, contents: {dt}", ) diff --git a/syncall/notion/__init__.py b/syncall/notion/__init__.py index e69de29..21857f6 100644 --- a/syncall/notion/__init__.py +++ b/syncall/notion/__init__.py @@ -0,0 +1 @@ +"""Notion subpackage.""" diff --git a/syncall/notion/notion_side.py b/syncall/notion/notion_side.py index d5db911..96a5197 100644 --- a/syncall/notion/notion_side.py +++ b/syncall/notion/notion_side.py @@ -1,9 +1,11 @@ from __future__ import annotations -from typing import Optional, Sequence, cast +from typing import TYPE_CHECKING, Optional, Sequence, cast from bubop import logger -from notion_client import Client + +if TYPE_CHECKING: + from notion_client import Client from syncall.notion.notion_todo_block import NotionTodoBlock from syncall.sync_side import SyncSide @@ -43,7 +45,7 @@ def start(self): def _get_todo_blocks(self) -> dict[NotionID, NotionTodoBlock]: all_todos = self.find_todos(page_contents=self._page_contents) # make sure that all IDs are valid and not None - assert all([todo.id is not None for todo in all_todos]) + assert all(todo.id is not None for todo in all_todos) return {cast(NotionID, todo.id): todo for todo in all_todos} @@ -58,7 +60,7 @@ def get_item( item_id: NotionID, use_cached: bool = False, ) -> Optional[NotionTodoBlock]: - """Return a single todo block""" + """Return a single todo block.""" if use_cached: return self._all_todo_blocks.get(item_id) @@ -66,9 +68,9 @@ def get_item( new_todo_block_item: NotionTodoBlockItem = self._client.blocks.retrieve(item_id) try: new_todo_block = NotionTodoBlock.from_raw_item(new_todo_block_item) - except RuntimeError: + except RuntimeError as err: # the to_do section is missing when the item is archived?! - raise KeyError + raise KeyError from err assert new_todo_block.id is not None self._all_todo_blocks[new_todo_block.id] = new_todo_block diff --git a/syncall/notion/notion_todo_block.py b/syncall/notion/notion_todo_block.py index bba86a2..f122ac3 100644 --- a/syncall/notion/notion_todo_block.py +++ b/syncall/notion/notion_todo_block.py @@ -5,11 +5,13 @@ if TYPE_CHECKING: import datetime + from item_synchronizer.types import ID + + from syncall.types import NotionRawItem, NotionTodoBlockItem, NotionTodoSection + from bubop import logger, parse_datetime -from item_synchronizer.types import ID from syncall.concrete_item import ConcreteItem, ItemKey, KeyType -from syncall.types import NotionRawItem, NotionTodoBlockItem, NotionTodoSection class NotionTodoBlock(ConcreteItem): @@ -19,7 +21,7 @@ def __init__( is_checked: bool, last_modified_date: datetime.datetime, plaintext: str, - id: ID | None = None, + id: ID | None = None, # noqa: A002 ): super().__init__( keys=( diff --git a/syncall/pdb_cli_utils.py b/syncall/pdb_cli_utils.py index eeda953..b46b85c 100644 --- a/syncall/pdb_cli_utils.py +++ b/syncall/pdb_cli_utils.py @@ -1,14 +1,14 @@ import sys -def run_pdb_on_error(type, value, tb): +def run_pdb_on_error(type, value, tb): # noqa: A002 if hasattr(sys, "ps1") or not sys.stderr.isatty(): # we are in interactive mode or we don't have a tty-like device, so we call the # default hook - print("Cannot enable the --pdb-on-error flag") + print("Cannot enable the --pdb-on-error flag") # noqa: T201 sys.__excepthook__(type, value, tb) else: - import pdb + import pdb # noqa: T100 import traceback traceback.print_exception(type, value, tb) diff --git a/syncall/scripts/tw_asana_sync.py b/syncall/scripts/tw_asana_sync.py index 028046e..ec17754 100644 --- a/syncall/scripts/tw_asana_sync.py +++ b/syncall/scripts/tw_asana_sync.py @@ -40,7 +40,7 @@ @opts_asana(hidden_gid=False) @opts_tw_filtering() @opts_miscellaneous("TW", "Asana") -def main( # noqa: PLR0915 +def main( # noqa: PLR0915, C901, PLR0912 asana_task_gid: str, asana_token: str, asana_workspace_gid: str, @@ -120,7 +120,6 @@ def main( # noqa: PLR0915 # initialize asana ----------------------------------------------------------------------- asana_client = asana.Client.access_token(asana_token) - asana_disable = asana_client.headers.get("Asana-Disable", "") asana_client.headers["Asana-Disable"] = ",".join( [ asana_client.headers.get("Asana-Disable", ""), diff --git a/syncall/scripts/tw_caldav_sync.py b/syncall/scripts/tw_caldav_sync.py index 5543318..1f16d1e 100644 --- a/syncall/scripts/tw_caldav_sync.py +++ b/syncall/scripts/tw_caldav_sync.py @@ -183,7 +183,7 @@ def main( if caldav_passwd is not None: logger.debug("Reading the caldav password from environment variable...") elif caldav_passwd_cmd is not None: - proc = subprocess.run( + proc = subprocess.run( # noqa: S602 caldav_passwd_cmd, shell=True, text=True, diff --git a/syncall/scripts/tw_gcal_sync.py b/syncall/scripts/tw_gcal_sync.py index 4d107e9..cd5a44e 100644 --- a/syncall/scripts/tw_gcal_sync.py +++ b/syncall/scripts/tw_gcal_sync.py @@ -48,7 +48,7 @@ @opts_miscellaneous(side_A_name="TW", side_B_name="Google Tasks") def main( gcal_calendar: str, - google_secret: str, + google_secret: str | None, oauth_port: int, tw_filter: str, tw_tags: list[str], diff --git a/syncall/scripts/tw_gtasks_sync.py b/syncall/scripts/tw_gtasks_sync.py index de6d690..72097b9 100644 --- a/syncall/scripts/tw_gtasks_sync.py +++ b/syncall/scripts/tw_gtasks_sync.py @@ -44,7 +44,7 @@ @opts_miscellaneous(side_A_name="TW", side_B_name="Google Tasks") def main( gtasks_list: str, - google_secret: str, + google_secret: str | None, oauth_port: int, tw_filter: str, tw_tags: list[str], diff --git a/syncall/side_helper.py b/syncall/side_helper.py index 3846909..1d3104a 100644 --- a/syncall/side_helper.py +++ b/syncall/side_helper.py @@ -14,7 +14,7 @@ class SideHelper: summary_key: str # Handy way to refer to the counterpart side other: Optional["SideHelper"] = None - ignore_keys: Sequence[str] = tuple() + ignore_keys: Sequence[str] = () def __str__(self): return str(self.name) diff --git a/syncall/taskwarrior/taskwarrior_side.py b/syncall/taskwarrior/taskwarrior_side.py index eb4d798..e8221d7 100644 --- a/syncall/taskwarrior/taskwarrior_side.py +++ b/syncall/taskwarrior/taskwarrior_side.py @@ -47,7 +47,7 @@ class TaskWarriorSide(SyncSide): def __init__( self, - tags: Sequence[str] = tuple(), + tags: Sequence[str] = (), project: Optional[str] = None, tw_filter: str = "", config_file_override: Optional[Path] = None, @@ -228,7 +228,7 @@ def add_item(self, item: ItemType) -> ItemType: logger.debug(f'Task "{new_id}" created - "{description[0:len_print]}"...') # explicitly mark as deleted - taskw doesn't like task_add(`status:deleted`) so we have - # TODO it in two steps + # to do it in two steps if curr_status == "deleted": logger.debug( f'Task "{new_id}" marking as deleted - "{description[0:len_print]}"...', diff --git a/syncall/tw_asana_utils.py b/syncall/tw_asana_utils.py index 6244d71..33e27dc 100644 --- a/syncall/tw_asana_utils.py +++ b/syncall/tw_asana_utils.py @@ -47,10 +47,8 @@ def convert_tw_to_asana(tw_item: TwItem) -> AsanaTask: as_created_at = tw_entry if tw_due is not None: - if isinstance(tw_due, datetime.datetime): - as_due_at = tw_due - else: - as_due_at = parse_datetime(tw_due) + as_due_at = tw_due if isinstance(tw_due, datetime.datetime) else parse_datetime(tw_due) + as_due_on = as_due_at.date() if isinstance(tw_modified, datetime.datetime): @@ -72,7 +70,7 @@ def convert_tw_to_asana(tw_item: TwItem) -> AsanaTask: ) -def convert_asana_to_tw(asana_task: AsanaTask) -> TwItem: +def convert_asana_to_tw(asana_task: AsanaTask) -> TwItem: # noqa: C901, PLR0912 # Extract Asana fields as_completed = asana_task["completed"] as_completed_at = asana_task["completed_at"] @@ -83,8 +81,7 @@ def convert_asana_to_tw(asana_task: AsanaTask) -> TwItem: as_name = asana_task["name"] # Declare Taskwarrior fields - tw_completed = None - tw_due = tw_item = None + tw_due = None tw_end = None tw_entry = None tw_modified = None diff --git a/syncall/tw_gcal_utils.py b/syncall/tw_gcal_utils.py index 404ebc4..3099832 100644 --- a/syncall/tw_gcal_utils.py +++ b/syncall/tw_gcal_utils.py @@ -153,13 +153,7 @@ def convert_gcal_to_tw( if gcal_summary.startswith(_prefix_title_success_str): gcal_summary = gcal_summary[len(_prefix_title_success_str) :] tw_item["description"] = gcal_summary - - # don't meddle with the 'entry' field - if set_scheduled_date: - date_key = "scheduled" - else: - date_key = "due" - + date_key = "scheduled" if set_scheduled_date else "due" end_time = GCalSide.get_event_time(gcal_item, t="end") tw_item[tw_duration_key] = end_time - GCalSide.get_event_time(gcal_item, t="start") diff --git a/syncall/tw_gtasks_utils.py b/syncall/tw_gtasks_utils.py index a2c94bb..387a55e 100644 --- a/syncall/tw_gtasks_utils.py +++ b/syncall/tw_gtasks_utils.py @@ -78,11 +78,7 @@ def convert_gtask_to_tw( # Description tw_item["description"] = gtasks_item["title"] - # don't meddle with the 'entry' field - if set_scheduled_date: - date_key = "scheduled" - else: - date_key = "due" + date_key = "scheduled" if set_scheduled_date else "due" # due/scheduled date due_date = GTasksSide.get_task_due_time(gtasks_item) diff --git a/syncall/tw_notion_utils.py b/syncall/tw_notion_utils.py index d6355ff..ea97b5d 100644 --- a/syncall/tw_notion_utils.py +++ b/syncall/tw_notion_utils.py @@ -24,10 +24,7 @@ def create_page(parent_page_id: str, title: str, client: Client) -> NotionPage: def convert_tw_to_notion(tw_item: TwItem) -> NotionTodoBlock: modified = tw_item["modified"] - if isinstance(modified, datetime.datetime): - dt = modified - else: - dt = parse_datetime(modified) + dt = modified if isinstance(modified, datetime.datetime) else parse_datetime(modified) return NotionTodoBlock( is_archived=False, diff --git a/syncall/tw_utils.py b/syncall/tw_utils.py index 8821c99..db00f6f 100644 --- a/syncall/tw_utils.py +++ b/syncall/tw_utils.py @@ -6,12 +6,13 @@ from __future__ import annotations import traceback -from typing import Optional, Sequence +from typing import TYPE_CHECKING, Optional, Sequence from uuid import UUID from bubop import logger -from syncall.types import TwItem +if TYPE_CHECKING: + from syncall.types import TwItem def get_tw_annotations_as_str(tw_item: TwItem) -> str: @@ -55,19 +56,19 @@ def extract_tw_fields_from_string(s: str) -> tuple[Sequence[str], str, Optional[ # strip whitespaces, empty lines lines = [line.strip() for line in s.split("\n") if line][0:] - i = 0 - for i, line in enumerate(lines): + _i = 0 + for _i, line in enumerate(lines): parts = line.split(":", maxsplit=1) if len(parts) == 2 and parts[0].lower().startswith("* annotation"): annotations.append(parts[1].strip()) else: break - if i == len(lines) - 1: + if _i == len(lines) - 1: return annotations, status, uuid # Iterate through rest of lines, find only the status and uuid ones - for line in lines[i:]: + for line in lines[_i:]: parts = line.split(":", maxsplit=1) if len(parts) == 2: start = parts[0].lower() diff --git a/syncall/types.py b/syncall/types.py index 03c4e1c..4e61d31 100644 --- a/syncall/types.py +++ b/syncall/types.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Any, Dict, Literal, Optional, Tuple, TypedDict, Union +import datetime +from typing import Any, Dict, Literal, Optional, Protocol, Tuple, TypedDict, Union from item_synchronizer.types import ID @@ -42,6 +43,9 @@ class TaskwarriorRawItem(TypedDict, total=False): # Item as returned from the Taskw Python API on tw.get_task(id=...) TaskwarriorRawTuple = Tuple[Optional[int], TaskwarriorRawItem] +# Google-related types ------------------------------------------------------------------------ +GoogleDateT = Union[str, dict, datetime.datetime] + # Google Calendar ----------------------------------------------------------------------------- GCalItem = Dict[str, Any] @@ -293,6 +297,11 @@ class AsanaRawTask(TypedDict): # Extras -------------------------------------------------------------------------------------- # Task as returned from get_task(id=...) -# TODO Are these types needed? They seem to be duplicates of TaskwarriorRawItem ... TwRawItem = Tuple[Optional[int], Dict[str, Any]] TwItem = Dict[str, Any] + + +# create a Protocol class for instances that have the __str__ method +class SupportsStr(Protocol): + def __str__(self) -> str: + ... diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..58de813 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests directory.""" diff --git a/tests/conftest.py b/tests/conftest.py index 012dfe6..8f3034f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,13 +6,13 @@ from bubop import PrefsManager from loguru import logger -from .conftest_fs import * -from .conftest_gcal import * -from .conftest_gkeep import * -from .conftest_gtasks import * -from .conftest_helpers import * -from .conftest_notion import * -from .conftest_tw import * +from .conftest_fs import * # noqa: F403 +from .conftest_gcal import * # noqa: F403 +from .conftest_gkeep import * # noqa: F403 +from .conftest_gtasks import * # noqa: F403 +from .conftest_helpers import * # noqa: F403 +from .conftest_notion import * # noqa: F403 +from .conftest_tw import * # noqa: F403 @pytest.fixture() diff --git a/tests/generic_test_case.py b/tests/generic_test_case.py index a98e9bc..38c5a5e 100644 --- a/tests/generic_test_case.py +++ b/tests/generic_test_case.py @@ -7,7 +7,6 @@ class GenericTestCase(unittest.TestCase): """Generic unittest class for the project.""" - # DATA_FILES_PATH = os.path.join(os.path.dirname(__file__), "test_data") DATA_FILES_PATH = Path(__file__).parent / "test_data" @classmethod diff --git a/tests/test_app_utils.py b/tests/test_app_utils.py index 88fa9f4..c087fa7 100644 --- a/tests/test_app_utils.py +++ b/tests/test_app_utils.py @@ -64,12 +64,11 @@ def test_inform_about_combination_name_usage(fs, caplog): sys.argv[0] = e c = "kalinuxta" inform_about_combination_name_usage(combination_name=c) - assert ( - e in caplog.text - and c in caplog.text - and COMBINATION_FLAGS[0] in caplog.text - and COMBINATION_FLAGS[1] in caplog.text - ) + + assert e in caplog.text + assert c in caplog.text + assert COMBINATION_FLAGS[0] in caplog.text + assert COMBINATION_FLAGS[1] in caplog.text def test_cache_or_reuse_cached_combination(fs, caplog, mock_prefs_manager): @@ -89,5 +88,6 @@ def test_cache_or_reuse_cached_combination(fs, caplog, mock_prefs_manager): custom_combination_savename=None, ) - assert "Loading cached configuration" in caplog.text and "1__2__3" in caplog.text + assert "Loading cached configuration" in caplog.text + assert "1__2__3" in caplog.text caplog.clear() diff --git a/tests/test_filesystem_file.py b/tests/test_filesystem_file.py index e20ddf7..dc8b33b 100644 --- a/tests/test_filesystem_file.py +++ b/tests/test_filesystem_file.py @@ -85,7 +85,7 @@ def test_fs_file_flush_change_title_content(python_path_with_content: Path): def test_fs_file_dict_fns(non_existent_python_path: Path): fs_file = FilesystemFile(path=non_existent_python_path, flush_on_instantiation=False) - assert set(("last_modified_date", "contents", "title", "id")).issubset( + assert {"last_modified_date", "contents", "title", "id"}.issubset( key for key in fs_file.keys() ) diff --git a/tests/test_filesystem_side.py b/tests/test_filesystem_side.py index cd1979f..b373d16 100644 --- a/tests/test_filesystem_side.py +++ b/tests/test_filesystem_side.py @@ -31,7 +31,6 @@ def test_create_new_item(fs_side: FilesystemSide): fs_side.add_item(new_item) # get the newly created item - make sure that its the same item as returned by - # get_all_items() all_items_after_addition = fs_side.get_all_items() assert len(all_items_after_addition) == prev_len + 1 fs_file = next(item for item in all_items_after_addition if item.id == new_id) diff --git a/tests/test_gcal.py b/tests/test_gcal.py index 3cfb206..0c5c6ee 100644 --- a/tests/test_gcal.py +++ b/tests/test_gcal.py @@ -1,19 +1,16 @@ import datetime -from typing import Any import syncall.google.gcal_side as side from bubop import is_same_datetime from dateutil.tz import gettz, tzutc +from syncall.types import GoogleDateT localzone = gettz("Europe/Athens") # Monkeypatch the function to always return Eruope/Athens for UT determinism def assume_local_tz_if_none_(dt: datetime.datetime): - if dt.tzinfo is None: - out = dt.replace(tzinfo=localzone) - else: - out = dt + out = dt if dt.tzinfo is not None else dt.replace(tzinfo=localzone) return out @@ -21,7 +18,7 @@ def assume_local_tz_if_none_(dt: datetime.datetime): side.assume_local_tz_if_none = assume_local_tz_if_none_ -def assert_dt(dt_given: Any, dt_expected: datetime.datetime): +def assert_dt(dt_given: GoogleDateT, dt_expected: datetime.datetime): parse_datetime = side.GCalSide.parse_datetime dt_dt_given = parse_datetime(dt_given) diff --git a/tests/test_tw_asana_conversions.py b/tests/test_tw_asana_conversions.py index 5cf92ff..77e937e 100644 --- a/tests/test_tw_asana_conversions.py +++ b/tests/test_tw_asana_conversions.py @@ -15,7 +15,7 @@ def get_keys_to_match(self): def load_sample_items(self): with (GenericTestCase.DATA_FILES_PATH / "sample_items.yaml").open() as fname: - conts = yaml.load(fname, Loader=yaml.Loader) + conts = yaml.load(fname, Loader=yaml.Loader) # noqa: S506 self.asana_task = conts["asana_task"] self.tw_item_expected = conts["tw_item_expected"] diff --git a/tests/test_tw_caldav_conversions.py b/tests/test_tw_caldav_conversions.py old mode 100755 new mode 100644 index 907681a..cfa0b35 --- a/tests/test_tw_caldav_conversions.py +++ b/tests/test_tw_caldav_conversions.py @@ -13,7 +13,7 @@ class TestConversions(GenericTestCase): def load_sample_items(self): with (GenericTestCase.DATA_FILES_PATH / "sample_items.yaml").open() as fname: - conts = yaml.load(fname, Loader=yaml.Loader) + conts = yaml.load(fname, Loader=yaml.Loader) # noqa: S506 self.caldav_item = conts["caldav_item"] self.tw_item_expected = conts["tw_item_expected"] diff --git a/tests/test_tw_notion.py b/tests/test_tw_notion.py index 36b37fd..13187da 100644 --- a/tests/test_tw_notion.py +++ b/tests/test_tw_notion.py @@ -1,10 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest from syncall.notion.notion_side import NotionSide from syncall.notion.notion_todo_block import NotionTodoBlock from syncall.tw_notion_utils import convert_notion_to_tw, convert_tw_to_notion -from syncall.types import NotionPageContents, NotionTodoBlockItem, TwItem + +if TYPE_CHECKING: + from syncall.types import NotionPageContents, NotionTodoBlockItem, TwItem # test conversions ---------------------------------------------------------------------------- diff --git a/tests/test_util_methods.py b/tests/test_util_methods.py old mode 100755 new mode 100644