From 120c85860465a9e50a8a897a58bc6bd99b6cca3e Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 2 Feb 2026 17:08:25 +0000 Subject: [PATCH 1/7] :shirt: Tighten up the linting rules --- Makefile | 2 +- pyproject.toml | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 35a4270..7aa435e 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ publish := uv publish --username=__token__ --keyring-provider=subprocess test := $(run) pytest python := $(run) python ruff := $(run) ruff -lint := $(ruff) check --select I +lint := $(ruff) check fmt := $(ruff) format mypy := $(run) mypy spell := $(run) codespell diff --git a/pyproject.toml b/pyproject.toml index 4fd5767..393fbd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,3 +89,26 @@ asyncio_default_fixture_loop_scope = "function" venvPath="." venv=".venv" exclude=[".venv"] + +[tool.ruff.lint] +select = [ + # pycodestyle + "E", + # Pyflakes + "F", + # pyupgrade + "UP", + # flake8-bugbear + "B", + # flake8-simplify + "SIM", + # isort + "I", +] +ignore = [ + # I think try...expect...pass reads far better. + "SIM105", +] + +[tool.ruff.lint.pycodestyle] +max-line-length = 120 From cde50b5f4bf89d7ea888809530bb839a03623fa8 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 2 Feb 2026 17:19:27 +0000 Subject: [PATCH 2/7] :shirt: Modernise some type hint imports --- src/braindrop/app/data/config.py | 2 +- src/braindrop/app/data/local.py | 3 ++- src/braindrop/app/data/raindrops.py | 4 +++- src/braindrop/app/screens/main.py | 2 +- src/braindrop/app/screens/raindrop_input.py | 2 +- src/braindrop/app/suggestions/tags.py | 7 ++++--- src/braindrop/app/widgets/raindrop_details.py | 3 ++- src/braindrop/raindrop/api.py | 3 ++- src/braindrop/raindrop/raindrop.py | 7 +++---- 9 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/braindrop/app/data/config.py b/src/braindrop/app/data/config.py index b7cdd3a..cc17ef4 100644 --- a/src/braindrop/app/data/config.py +++ b/src/braindrop/app/data/config.py @@ -2,12 +2,12 @@ ############################################################################## # Python imports. +from collections.abc import Iterator from contextlib import contextmanager from dataclasses import asdict, dataclass, field from functools import lru_cache from json import dumps, loads from pathlib import Path -from typing import Iterator ############################################################################## # Local imports. diff --git a/src/braindrop/app/data/local.py b/src/braindrop/app/data/local.py index c89ef76..d4c7495 100644 --- a/src/braindrop/app/data/local.py +++ b/src/braindrop/app/data/local.py @@ -2,10 +2,11 @@ ############################################################################## # Python imports. +from collections.abc import Callable, Iterable, Iterator from datetime import datetime from json import dumps, loads from pathlib import Path -from typing import Any, Callable, Final, Iterable, Iterator, Self +from typing import Any, Final, Self ############################################################################## # pytz imports. diff --git a/src/braindrop/app/data/raindrops.py b/src/braindrop/app/data/raindrops.py index 02c93ad..82b8279 100644 --- a/src/braindrop/app/data/raindrops.py +++ b/src/braindrop/app/data/raindrops.py @@ -6,9 +6,11 @@ ############################################################################## # Python imports. +from collections import Counter +from collections.abc import Callable, Iterable, Iterator from dataclasses import dataclass from functools import total_ordering -from typing import Callable, Counter, Iterable, Iterator, Self, TypeAlias +from typing import Self, TypeAlias ############################################################################## # Local imports. diff --git a/src/braindrop/app/screens/main.py b/src/braindrop/app/screens/main.py index b287ee0..0269747 100644 --- a/src/braindrop/app/screens/main.py +++ b/src/braindrop/app/screens/main.py @@ -2,7 +2,7 @@ ############################################################################## # Python imports. -from typing import Callable +from collections.abc import Callable from webbrowser import open as open_url ############################################################################## diff --git a/src/braindrop/app/screens/raindrop_input.py b/src/braindrop/app/screens/raindrop_input.py index 33aebf7..ee9e70d 100644 --- a/src/braindrop/app/screens/raindrop_input.py +++ b/src/braindrop/app/screens/raindrop_input.py @@ -2,7 +2,7 @@ ############################################################################## # Python imports. -from typing import Iterator +from collections.abc import Iterator ############################################################################## # httpx imports. diff --git a/src/braindrop/app/suggestions/tags.py b/src/braindrop/app/suggestions/tags.py index 0e4c92e..94d56dd 100644 --- a/src/braindrop/app/suggestions/tags.py +++ b/src/braindrop/app/suggestions/tags.py @@ -2,8 +2,9 @@ ############################################################################## # Python imports. -import re -from typing import Final, Iterable, Pattern +from collections.abc import Iterable +from re import Pattern, compile +from typing import Final ############################################################################## # Textual imports. @@ -33,7 +34,7 @@ def __init__(self, tags: Iterable[Tag | TagCount], use_cache: bool = True) -> No self._tags = [tag.tag if isinstance(tag, TagCount) else tag for tag in tags] """The tags to take suggestions from.""" - _SUGGESTABLE: Final[Pattern[str]] = re.compile(r".*[^,\s]$") + _SUGGESTABLE: Final[Pattern[str]] = compile(r".*[^,\s]$") """Regular expression to test if a value deserves a suggestion.""" async def get_suggestion(self, value: str) -> str | None: diff --git a/src/braindrop/app/widgets/raindrop_details.py b/src/braindrop/app/widgets/raindrop_details.py index 0aa12bd..1b1d75a 100644 --- a/src/braindrop/app/widgets/raindrop_details.py +++ b/src/braindrop/app/widgets/raindrop_details.py @@ -2,8 +2,9 @@ ############################################################################## # Python imports. +from collections.abc import Callable from datetime import datetime -from typing import Any, Callable, Final +from typing import Any, Final ############################################################################## # Humanize imports. diff --git a/src/braindrop/raindrop/api.py b/src/braindrop/raindrop/api.py index 088c355..4102526 100644 --- a/src/braindrop/raindrop/api.py +++ b/src/braindrop/raindrop/api.py @@ -13,11 +13,12 @@ ############################################################################## # Python imports. from asyncio import sleep +from collections.abc import Awaitable, Callable from dataclasses import dataclass from http import HTTPStatus from json import loads from ssl import SSLCertVerificationError -from typing import Any, Awaitable, Callable, Final, Literal +from typing import Any, Final, Literal ############################################################################## # HTTPX imports. diff --git a/src/braindrop/raindrop/raindrop.py b/src/braindrop/raindrop/raindrop.py index 4535857..05fa759 100644 --- a/src/braindrop/raindrop/raindrop.py +++ b/src/braindrop/raindrop/raindrop.py @@ -6,9 +6,10 @@ ############################################################################## # Python imports. +from collections.abc import Iterable from dataclasses import dataclass, field, replace from datetime import datetime -from typing import Any, Final, Iterable, Literal, TypeAlias +from typing import Any, Final, Literal ############################################################################## # Local imports. @@ -17,9 +18,7 @@ from .time_tools import get_time, json_time ############################################################################## -RaindropType: TypeAlias = Literal[ - "link", "article", "image", "video", "document", "audio" -] +type RaindropType = Literal["link", "article", "image", "video", "document", "audio"] """The type of a Raindrop.""" From 8cd35f36fd442c37a4aaa42fa35be21057e0e89d Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 2 Feb 2026 17:20:28 +0000 Subject: [PATCH 3/7] :hammer: Swap lru_cache for cache --- src/braindrop/app/data/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/braindrop/app/data/config.py b/src/braindrop/app/data/config.py index cc17ef4..342022a 100644 --- a/src/braindrop/app/data/config.py +++ b/src/braindrop/app/data/config.py @@ -5,7 +5,7 @@ from collections.abc import Iterator from contextlib import contextmanager from dataclasses import asdict, dataclass, field -from functools import lru_cache +from functools import cache from json import dumps, loads from pathlib import Path @@ -63,7 +63,7 @@ def save_configuration(configuration: Configuration) -> Configuration: ############################################################################## -@lru_cache(maxsize=None) +@cache def load_configuration() -> Configuration: """Load the configuration. From d2544de69517778130ff3c32a3197c22d8e6afdd Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 2 Feb 2026 17:21:13 +0000 Subject: [PATCH 4/7] :truck: Rename IOError to OSError --- src/braindrop/app/braindrop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/braindrop/app/braindrop.py b/src/braindrop/app/braindrop.py index 0a82294..ff87155 100644 --- a/src/braindrop/app/braindrop.py +++ b/src/braindrop/app/braindrop.py @@ -97,7 +97,7 @@ def api_token(self) -> str | None: return self.environmental_token() or token_file().read_text( encoding="utf-8" ) - except IOError: + except OSError: pass return None From 97c4e4d3922d32d187b758572883f1f773f61a4c Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 2 Feb 2026 17:22:55 +0000 Subject: [PATCH 5/7] :shirt: Clean up a couple of raises --- src/braindrop/raindrop/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/braindrop/raindrop/api.py b/src/braindrop/raindrop/api.py index 4102526..d2e431f 100644 --- a/src/braindrop/raindrop/api.py +++ b/src/braindrop/raindrop/api.py @@ -141,7 +141,7 @@ async def _call( int(error.response.headers["Retry-After"]) if "Retry-After" in error.response.headers else None - ) + ) from None else: raise self.RequestError(str(error)) from None @@ -333,7 +333,7 @@ def gndn(_: int) -> None: if limit.retry_after is None: raise self.RequestError( "Raindrop.io API limit exceeded with no option to retry" - ) + ) from None count_update(-len(raindrops)) await sleep(limit.retry_after) continue From 5d2339637a0478cece8a077950ffc3893916f99e Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 2 Feb 2026 17:24:06 +0000 Subject: [PATCH 6/7] :hammer: Swap a TypeAlias for a type --- src/braindrop/app/data/raindrops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/braindrop/app/data/raindrops.py b/src/braindrop/app/data/raindrops.py index 82b8279..2656f71 100644 --- a/src/braindrop/app/data/raindrops.py +++ b/src/braindrop/app/data/raindrops.py @@ -10,7 +10,7 @@ from collections.abc import Callable, Iterable, Iterator from dataclasses import dataclass from functools import total_ordering -from typing import Self, TypeAlias +from typing import Self ############################################################################## # Local imports. @@ -83,7 +83,7 @@ def __eq__(self, value: object, /) -> bool: ############################################################################## -Filters: TypeAlias = tuple["Filter", ...] +type Filters = tuple["Filter", ...] """The type of a collection of filters.""" From 5f7d4add96ba10acde67e1e1372506e3e6de745f Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 2 Feb 2026 17:26:19 +0000 Subject: [PATCH 7/7] :shirt: Let the linter tail wag the code dog --- src/braindrop/app/widgets/raindrops_view.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/braindrop/app/widgets/raindrops_view.py b/src/braindrop/app/widgets/raindrops_view.py index 477093e..e825b08 100644 --- a/src/braindrop/app/widgets/raindrops_view.py +++ b/src/braindrop/app/widgets/raindrops_view.py @@ -96,9 +96,12 @@ def prompt(self) -> Group: if self._raindrop.excerpt: excerpt = Table.grid() excerpt.add_column(ratio=1, no_wrap=self._compact) - excerpt.add_row( - f"[dim]{escape(self._raindrop.excerpt.splitlines()[0] if self._compact else self._raindrop.excerpt)}[/dim]" + content = escape( + self._raindrop.excerpt.splitlines()[0] + if self._compact + else self._raindrop.excerpt ) + excerpt.add_row(f"[dim]{content}[/dim]") body.append(excerpt) details = Table.grid(expand=True)