Compare commits
2 Commits
5214ad2340
...
58a952d5d3
Author | SHA1 | Date | |
---|---|---|---|
|
58a952d5d3 | ||
|
cbfc5edbe5 |
75
docs/configuration.rst
Normal file
75
docs/configuration.rst
Normal 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!
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
-----
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ Table of Contents
|
|||
|
||||
installation
|
||||
getting-started
|
||||
configuration
|
||||
contributing
|
||||
release-notes
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -101,5 +101,7 @@ module = [
|
|||
"gentoopm",
|
||||
"gentoopm.*",
|
||||
"pkgcheck",
|
||||
# indirect deps
|
||||
"argcomplete",
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
|
9
tests/data/flag_sample.toml
Normal file
9
tests/data/flag_sample.toml
Normal 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"
|
|
@ -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"}
|
||||
|
|
Loading…
Reference in New Issue
Block a user