Compare commits

...

2 Commits

Author SHA1 Message Date
Anna “CyberTailor”
58a952d5d3
cli: add custom global flags
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-08 19:27:51 +05:00
Anna “CyberTailor”
cbfc5edbe5
cli: silence pkgcheck status messages 2024-01-31 06:24:09 +05:00
13 changed files with 245 additions and 73 deletions

75
docs/configuration.rst Normal file
View File

@ -0,0 +1,75 @@
.. SPDX-FileCopyrightText: 2024 Anna <cyber@sysrq.in>
.. SPDX-License-Identifier: WTFPL
.. No warranty.
Configuration
=============
Files
-----
The `TOML`_ format is used to configure ``find-work``.
.. _TOML: https://toml.io/
The tool reads configuration from the following files, in that order:
* *built-in* (:gitweb:`find_work/data/default_config.toml`)
* :file:`/etc/find-work/config.toml`
* :file:`~/.config/find-work/config.toml`
New values are merged with old ones, replacing them on conflicts.
Custom flags
------------
You'll be probably tired typing your email every time with the ``-m`` flag.
Let's add a new command-line flag to make life easier:
.. code-block:: toml
[flag.larry]
# This will be the help text for your new global flag
description = "Only match packages maintained by Larry the Cow."
# Add some shortcuts to your new global flag.
shortcuts = ["-L"]
# Redefine a global option.
# Always use the long (double-dash) option name.
params.maintainer = "larry@gentoo.org"
Save the config file and list bugs assigned to Larry:
.. prompt:: bash
find-work --larry bugzilla ls
Custom aliases
--------------
You can create new commands from existing ones!
.. code-block:: toml
[alias.guru-outdated]
# This will be the help text for your new command.
description = "Find outdated packages in GURU with Repology."
# Add some shortcuts to your new command.
shortcuts = ["guru-out"]
# Here we set the target command with Python syntax.
command = "find_work.cli.repology.outdated"
# And here we pass a static value directly to the internal options.
options.repology.repo = "gentoo_ovl_guru"
Save the config file and run your new command:
.. prompt:: bash
find-work -I execute guru-outdated
As you can see, you need to be somewhat familiar with the source code to add new
commands. Happy hacking!

View File

@ -33,39 +33,3 @@ You can use command aliases, for example:
All data from APIs is cached for a day, so don't hesitate running the command
again and again!
Custom aliases
--------------
You can create new commands from existing ones!
To start, launch your favorite editor at :file:`~/.config/find-work/config.toml`
and write your first alias:
.. code-block:: toml
[alias.guru-outdated]
# This will be the help text for your new command.
description = "Find outdated packages in GURU with Repology."
# Add some shortcuts to your new command.
shortcuts = ["guru-out"]
# Here we set the target command with Python syntax.
command = "find_work.cli.repology.outdated"
# And here we pass a static value directly to the internal options.
options.repology.repo = "gentoo_ovl_guru"
Save the config file and run your new command:
.. prompt:: bash
find-work -I execute guru-outdated
As you can see, you need to be somewhat familiar with the utility's source code.
Happy hacking!
.. tip::
See :gitweb:`find_work/data/default_config.toml` for pre-defined aliases.

View File

@ -8,8 +8,12 @@ Release Notes
0.6.0
-----
* **New:** Define custom global flags to override global options.
* **New:** Filter ``pkgcheck`` results by keyword or message.
* Silence pkgcore warnings and pkgcheck status messages.
0.5.0
-----

View File

@ -15,6 +15,7 @@ Table of Contents
installation
getting-started
configuration
contributing
release-notes

View File

@ -5,6 +5,7 @@
import logging
import os
from datetime import date
from typing import Any
import click
from click_aliases import ClickAliasedGroup
@ -14,7 +15,8 @@ import find_work.cli.execute
import find_work.cli.pgo
import find_work.cli.pkgcheck
import find_work.cli.repology
from find_work.cli import Options
from find_work.cli import Options, apply_custom_flags
from find_work.config import load_config
from find_work.constants import VERSION
@ -28,22 +30,28 @@ from find_work.constants import VERSION
help="Only match installed packages.")
@click.version_option(VERSION, "-V", "--version")
@click.pass_context
def cli(ctx: click.Context, maintainer: str | None,
quiet: bool, installed: bool) -> None:
@apply_custom_flags
def cli(ctx: click.Context, **kwargs: Any) -> None:
""" Personal advice utility for Gentoo package maintainers. """
# Process custom global flags
for flag in load_config().flags:
if ctx.params[flag.name]:
for opt, val in flag.params.items():
ctx.params[opt] = val
ctx.ensure_object(Options)
options: Options = ctx.obj
options.verbose = not quiet
options.only_installed = installed
options.verbose = not ctx.params["quiet"]
options.only_installed = ctx.params["installed"]
if any(var in os.environ for var in ["NOCOLOR", "NO_COLOR"]):
options.colors = False
options.cache_key.feed(date.today().toordinal())
if maintainer:
options.maintainer = maintainer
options.cache_key.feed_option("maintainer", maintainer)
if ctx.params["maintainer"]:
options.maintainer = ctx.params["maintainer"]
options.cache_key.feed_option("maintainer", options.maintainer)
# silence pkgcore
pkgcore_logger = logging.getLogger("pkgcore")

View File

@ -6,7 +6,7 @@
import threading
from abc import ABC, abstractmethod
from collections.abc import Generator
from collections.abc import Callable, Generator
from contextlib import contextmanager
from dataclasses import field
from functools import cached_property
@ -17,6 +17,7 @@ import click
from pydantic.dataclasses import dataclass
from find_work.cache import CacheKey
from find_work.config import load_config
class ProgressDots:
@ -204,3 +205,15 @@ class Options(OptionsBase):
self.secho("Congrats! You have nothing to do!", fg="green")
case _:
raise TypeError(f"Unknown message identifier: {msgid}")
def apply_custom_flags(callback: Callable) -> Callable:
"""
A decorator function to load custom global flags from configuration files.
"""
for flag in load_config().flags:
names = [f"--{flag.name}"]
names += flag.shortcuts
callback = click.option(*names, help=flag.description, is_flag=True)(callback)
return callback

View File

@ -4,23 +4,15 @@
""" CLI subcommand for executing custom aliases. """
import tomllib
from collections.abc import Callable
from importlib import import_module
from importlib.resources import files
from pathlib import Path
from typing import Any
import click
import gentoopm
from click_aliases import ClickAliasedGroup
from deepmerge import always_merger
from platformdirs import PlatformDirs
import find_work.data
from find_work.cli import Options
from find_work.config import Config, ConfigAlias, ConfigModuleOption
from find_work.constants import DEFAULT_CONFIG, ENTITY, PACKAGE
from find_work.config import ConfigAlias, ConfigModuleOption, load_config
from find_work.types import CliOptionKind
@ -76,21 +68,5 @@ def load_aliases(group: ClickAliasedGroup) -> None:
:param group: click group for new commands
"""
default_config = files(find_work.data).joinpath(DEFAULT_CONFIG).read_text()
toml = tomllib.loads(default_config)
pm = gentoopm.get_package_manager()
system_config = Path(pm.root) / "etc" / PACKAGE / "config.toml"
if system_config.is_file():
with open(system_config, "rb") as file:
always_merger.merge(toml, tomllib.load(file))
dirs = PlatformDirs(PACKAGE, ENTITY, roaming=True)
user_config = dirs.user_config_path / "config.toml"
if user_config.is_file():
with open(user_config, "rb") as file:
always_merger.merge(toml, tomllib.load(file))
config = Config(toml)
for alias in config.aliases:
for alias in load_config().aliases:
group.command(aliases=alias.shortcuts)(_callback_from_config(alias))

View File

@ -19,6 +19,7 @@ def _do_scan(options: Options) -> SortedDict[str, SortedSet]:
repo_obj = pm.repositories[options.pkgcheck.repo]
cli_opts = [
"--quiet",
"--repo", options.pkgcheck.repo,
"--scope", "pkg,ver",
"--filter", "latest", # TODO: become version-aware

View File

@ -4,10 +4,19 @@
""" Configuration parsing and validation. """
import tomllib
from abc import ABC
from functools import cached_property
from functools import cache, cached_property
from importlib.resources import files
from pathlib import Path
from typing import Any
import gentoopm
from deepmerge import always_merger
from platformdirs import PlatformDirs
import find_work.data
from find_work.constants import DEFAULT_CONFIG, ENTITY, PACKAGE
from find_work.types import CliOptionKind
@ -110,7 +119,7 @@ class ConfigAlias(ConfigBase):
for opt_name, opt_value in opts.items():
if isinstance(opt_value, dict) and len(opt_value) != 1:
raise self._value_error(f"module.{opt_name}", "invalid value")
raise self._value_error(f"{module}.{opt_name}", "invalid value")
result.append(ConfigModuleOption(module, opt_name, opt_value,
context=self._context + "options"))
return result
@ -140,6 +149,45 @@ class ConfigAlias(ConfigBase):
return self._get("shortcuts", list, default=[])
class ConfigFlag(ConfigBase):
""" Basic custom global flags parser and validator. """
def __init__(self, name: str, params: dict, *, context: str = ""):
self.name: str = name
self._obj: dict = params
self._context: str = f"{context}.{name}."
@cached_property
def params(self) -> dict[str, Any]:
"""
Load new global flag's module options from the configuration.
"""
result: dict[str, Any] = {}
opts = self._get("params", dict)
for opt_name, opt_value in opts.items():
result[opt_name] = opt_value
return result
@cached_property
def description(self) -> str:
"""
Load new global option's help text from the configuration.
"""
return self._get("description", str)
@cached_property
def shortcuts(self) -> list[str]:
"""
Load new global option's aliases from the configuration.
"""
return self._get("shortcuts", list, default=[])
class Config(ConfigBase):
""" Basic configuration parser and validator. """
@ -158,3 +206,39 @@ class Config(ConfigBase):
result.append(ConfigAlias(name, params,
context=self._context + "alias"))
return result
@cached_property
def flags(self) -> list[ConfigFlag]:
"""
Load custom flags from the configuration.
"""
result: list[ConfigFlag] = []
for name, params in self._get("flag", dict, default={}).items():
result.append(ConfigFlag(name, params,
context=self._context + "flag"))
return result
@cache
def load_config() -> Config:
"""
Load configuration files.
"""
default_config = files(find_work.data).joinpath(DEFAULT_CONFIG).read_text()
toml = tomllib.loads(default_config)
pm = gentoopm.get_package_manager()
system_config = Path(pm.root) / "etc" / PACKAGE / "config.toml"
if system_config.is_file():
with open(system_config, "rb") as file:
always_merger.merge(toml, tomllib.load(file))
dirs = PlatformDirs(PACKAGE, ENTITY, roaming=True)
user_config = dirs.user_config_path / "config.toml"
if user_config.is_file():
with open(user_config, "rb") as file:
always_merger.merge(toml, tomllib.load(file))
return Config(toml)

View File

@ -2,6 +2,17 @@
# SPDX-FileCopyrightText: 2024 Anna <cyber@sysrq.in>
# No warranty
# Global Flags
[flag.orphaned]
description = "Only match orphaned (maintainer-needed) packages."
shortcuts = ["-O"]
params.maintainer = "maintainer-needed@gentoo.org"
# Command Aliases
[alias.bump-requests]
command = "find_work.cli.bugzilla.ls"
description = "Find packages with version bump requests on Bugzilla."

View File

@ -101,5 +101,7 @@ module = [
"gentoopm",
"gentoopm.*",
"pkgcheck",
# indirect deps
"argcomplete",
]
ignore_missing_imports = true

View File

@ -0,0 +1,9 @@
# SPDX-License-Identifier: WTFPL
# SPDX-FileCopyrightText: 2024 Anna <cyber@sysrq.in>
# No warranty
[flag.sample]
description = "Sample global flag."
shortcuts = ["-s"]
params.key1 = "val1"
params.key2 = "val2"

View File

@ -51,3 +51,27 @@ def test_alias_sample():
assert flag_opt[0].module == "sample"
assert flag_opt[0].kind == CliOptionKind.FLAG
assert flag_opt[0].value == ["-f", "--flag"]
def test_flag_empty():
assert not Config({}).flags
def test_flag_type_error():
with pytest.raises(TypeError):
Config({"flag": "hello"}).flags
def test_flag_sample():
path = Path(__file__).parent / "data" / "flag_sample.toml"
with open(path, "rb") as file:
toml = tomllib.load(file)
config = Config(toml)
assert len(config.flags) == 1
flag = config.flags.pop()
assert flag.name == "sample"
assert flag.description == "Sample global flag."
assert flag.shortcuts == ["-s"]
assert flag.params == {"key1": "val1", "key2": "val2"}