From 4935f3fe4576cca37c6e468d0c37b330336511bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anna=20=E2=80=9CCyberTailor=E2=80=9D?= Date: Tue, 23 Apr 2024 23:22:22 +0500 Subject: [PATCH] cli: add version part filter Bug: https://bugs.sysrq.in/show_bug.cgi?id=4 --- docs/release-notes.rst | 4 +++- find_work/cli/__init__.py | 15 +++++++++++- find_work/cli/pgo.py | 27 ++++++++++++++++----- find_work/cli/repology.py | 27 ++++++++++++++++----- find_work/types.py | 48 ++++++++++++++++++++++++++++++++++--- man/find-work.1 | 50 +++++++++++++++++++++++++++++++++++---- 6 files changed, 150 insertions(+), 21 deletions(-) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 7683972..3801930 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -5,9 +5,11 @@ Release Notes ============= -0.6.2 +0.7.0 ----- +* **New**: Filter oudated packages by version part (:bug:`4`). + *Modules changelog:* * **pgo**: diff --git a/find_work/cli/__init__.py b/find_work/cli/__init__.py index a1a295b..91a97b5 100644 --- a/find_work/cli/__init__.py +++ b/find_work/cli/__init__.py @@ -68,6 +68,9 @@ class OptionsBase(ABC): class ModuleOptionsBase(OptionsBase, ABC): """ Base class for module-specific options. """ + #: Extra options used in the command scope. + extra_options: dict[str, Any] | None = None + @property @abstractmethod def cache_order(self) -> list[str]: @@ -95,6 +98,15 @@ class BugzillaOptions(ModuleOptionsBase): return ["chronological_sort", "short_desc", "product", "component"] +@dataclass +class PgoOptions(ModuleOptionsBase): + """ Gentoo Packages subcommand options. """ + + @cached_property + def cache_order(self) -> list[str]: + return [] + + @dataclass class PkgcheckOptions(ModuleOptionsBase): """ pkgcheck subcommand options. """ @@ -155,8 +167,9 @@ class Options(OptionsBase): # Byte string used for creating cache key. cache_key: CacheKey = field(default_factory=CacheKey) - # Subcommand options. + #: Subcommand options. bugzilla: BugzillaOptions = field(default_factory=BugzillaOptions) + pgo: PgoOptions = field(default_factory=PgoOptions) pkgcheck: PkgcheckOptions = field(default_factory=PkgcheckOptions) repology: RepologyOptions = field(default_factory=RepologyOptions) diff --git a/find_work/cli/pgo.py b/find_work/cli/pgo.py index f8d912e..24573c6 100644 --- a/find_work/cli/pgo.py +++ b/find_work/cli/pgo.py @@ -18,7 +18,7 @@ from find_work.cache import ( ) from find_work.cli import Message, Options, ProgressDots from find_work.constants import PGO_BASE_URL, PGO_API_URL -from find_work.types import VersionBump +from find_work.types import VersionBump, VersionPart from find_work.utils import aiohttp_session @@ -61,6 +61,9 @@ async def _outdated(options: Options) -> None: "Filtering by maintainer is not implemented for this command" ) + if (extra_options := options.pgo.extra_options) is not None: + version_part: VersionPart | None = extra_options.get("version_part") + dots = ProgressDots(options.verbose) options.say(Message.CACHE_LOAD) @@ -78,14 +81,18 @@ async def _outdated(options: Options) -> None: with dots(): write_json_cache(data, options.cache_key) - outdated_set = _collect_version_bumps(data, options) - for bump in outdated_set: + no_work = True + for bump in _collect_version_bumps(data, options): + if version_part and not bump.changed(version_part): + continue + options.echo(bump.atom + " ", nl=False) options.secho(bump.old_version, fg="red", nl=False) options.echo(" → ", nl=False) options.secho(bump.new_version, fg="green") + no_work = False - if len(outdated_set) == 0: + if no_work: options.say(Message.NO_WORK) @@ -168,10 +175,18 @@ async def _stabilization(options: Options) -> None: @click.command() +@click.option("-F", "--filter", "version_part", + type=click.Choice(["major", "minor", "patch"]), + help="Version part filter.") @click.pass_obj -def outdated(options: Options) -> None: - """ Find outdated packages. """ +def outdated(options: Options, version_part: VersionPart | None = None) -> None: + """ + Find outdated packages. + """ + options.cache_key.feed("outdated") + + options.pgo.extra_options = {"version_part": version_part} asyncio.run(_outdated(options)) diff --git a/find_work/cli/repology.py b/find_work/cli/repology.py index a0a0fec..4467e46 100644 --- a/find_work/cli/repology.py +++ b/find_work/cli/repology.py @@ -21,7 +21,7 @@ from find_work.cache import ( write_json_cache, ) from find_work.cli import Message, Options, ProgressDots -from find_work.types import VersionBump +from find_work.types import VersionBump, VersionPart from find_work.utils import aiohttp_session @@ -87,6 +87,9 @@ def _collect_version_bumps(data: Iterable[set[Package]], async def _outdated(options: Options) -> None: dots = ProgressDots(options.verbose) + if (extra_options := options.repology.extra_options) is not None: + version_part: VersionPart | None = extra_options.get("version_part") + options.say(Message.CACHE_LOAD) with dots(): cached_data = read_json_cache(options.cache_key) @@ -107,20 +110,32 @@ async def _outdated(options: Options) -> None: json_data = _projects_to_json(data) write_json_cache(json_data, options.cache_key) - outdated_set = _collect_version_bumps(data.values(), options) - for bump in outdated_set: + no_work = True + for bump in _collect_version_bumps(data.values(), options): + if version_part and not bump.changed(version_part): + continue + options.echo(bump.atom + " ", nl=False) options.secho(bump.old_version, fg="red", nl=False) options.echo(" → ", nl=False) options.secho(bump.new_version, fg="green") + no_work = False - if len(outdated_set) == 0: + if no_work: options.say(Message.NO_WORK) @click.command() +@click.option("-F", "--filter", "version_part", + type=click.Choice(["major", "minor", "patch"]), + help="Version part filter.") @click.pass_obj -def outdated(options: Options) -> None: - """ Find outdated packages. """ +def outdated(options: Options, version_part: VersionPart | None = None) -> None: + """ + Find outdated packages. + """ + options.cache_key.feed("outdated") + + options.repology.extra_options = {"version_part": version_part} asyncio.run(_outdated(options)) diff --git a/find_work/types.py b/find_work/types.py index a7851a4..4113ec6 100644 --- a/find_work/types.py +++ b/find_work/types.py @@ -3,23 +3,65 @@ # No warranty """ -Type definitions for the application, implemented (mostly) as Pydantic models. +Type definitions for the application, implemented as enums and Pydantic models. """ from dataclasses import field -from enum import Enum, auto +from enum import Enum, StrEnum, auto +from itertools import zip_longest from pydantic.dataclasses import dataclass +class VersionPart(StrEnum): + MAJOR = auto() + MINOR = auto() + PATCH = auto() + + @dataclass(frozen=True, order=True) class VersionBump: - """ Version bump representation for a Gentoo repository. """ + """ + Version bump representation for a Gentoo repository. + """ atom: str old_version: str = field(compare=False) new_version: str = field(compare=False) + def changed(self, stop_after_part: VersionPart) -> bool: + """ + Roughly determine whether versions differ up to the given part. + + >>> VersionBump("foo", "1.3.2", "1.4").changed(VersionPart.PATCH) + True + >>> VersionBump("foo", "1.3.2", "2.0").changed(VersionPart.MINOR) + True + >>> VersionBump("foo", "1", "1.0.1").changed(VersionPart.MINOR) + False + >>> VersionBump("foo", "1", "1.1").changed(VersionPart.MINOR) + True + + Remember that it doesn't always work correctly: + + >>> VersionBump("foo", "1", "1.00").changed(VersionPart.MINOR) + True + """ + + def split_version(version: str) -> list[str]: + return version.replace("-", ".").replace("_", ".").split(".") + + old_parts = split_version(self.old_version) + new_parts = split_version(self.new_version) + + parts = list(zip_longest(old_parts, new_parts, fillvalue="0")) + stop_index = list(VersionPart).index(stop_after_part) + for old, new in parts[:stop_index + 1]: + if old != new: + return True + + return False + @dataclass(frozen=True, order=True) class BugView: diff --git a/man/find-work.1 b/man/find-work.1 index e3f5fef..af7acc6 100644 --- a/man/find-work.1 +++ b/man/find-work.1 @@ -2,7 +2,7 @@ .\" SPDX-FileCopyrightText: 2024 Anna .\" SPDX-License-Identifier: WTFPL .\" No warranty -.Dd January 14, 2024 +.Dd April 23, 2024 .Dt FIND-WORK 1 .Os .Sh NAME @@ -17,9 +17,11 @@ .Ar module .Op Ar arg ... .Ar command +.Op Ar arg ... .Sh DESCRIPTION .Nm -is a fun little tool that lets you discover what you can do for Gentoo as a package mantainer. +is a fun little tool +that lets you discover what you can do for Gentoo as a package mantainer. It contains filters to show only packages you might be interested in. .Pp .Nm @@ -84,8 +86,28 @@ This module uses Gentoo Packages API to find work. .Ar command can be one of the following: .Bl -tag -width Ds -.It Ic outdated Pq alias: Ic out , Ic o +.It Xo +.Ic outdated +.Fl F Ar part +.Xc +.Dl Pq alias: Ic out , Ic o +.Pp Find outdated packages. +.Pp +This command accepts the following options: +.Bl -tag width Ds +.It Fl F Ar part , Fl -filter Ar part +Least important version part +.Po +major, +minor, +patch +.Pc +change to be displayed. +Version part matching is implemented very roughly +and can lead to false matches. +.El +. .It Ic stabilization Pq alias: Ic stab , Ic s Find stabilization candidates. .El @@ -133,8 +155,28 @@ This module uses Repology API to find work. .Ar command can be one of the following: .Bl -tag -width Ds -.It Ic outdated Pq alias: Ic out , Ic o +.It Xo +.Ic outdated +.Fl F Ar part +.Xc +.Dl Pq alias: Ic out , Ic o +.Pp Find outdated packages. +.Pp +This command accepts the following options: +.Bl -tag width Ds +.It Fl F Ar part , Fl -filter Ar part +Least important version part +.Po +major, +minor, +patch +.Pc +change to be displayed. +Version part matching is implemented very roughly +and can lead to false matches. +.El +. .El .Pp The options for