Skip to content
Snippets Groups Projects
config.py 5.17 KiB
# SPDX-FileCopyrightText: Copyright © 2023 Idiap Research Institute <contact@idiap.ch>
#
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations

import importlib.metadata
import inspect
import typing

import click

from clapper.click import AliasedGroup, verbosity_option
from clapper.logging import setup

logger = setup(__name__.split(".")[0], format="%(levelname)s: %(message)s")


@click.group(cls=AliasedGroup)
def config():
    """Commands for listing, describing and copying configuration resources."""
    pass


@config.command(
    epilog="""Examples:

\b
  1. Lists all configuration resources (type: ptbench.config) installed:

     .. code:: sh

        ptbench config list


\b
  2. Lists all configuration resources and their descriptions (notice this may
     be slow as it needs to load all modules once):

     .. code:: sh

        ptbench config list -v

"""
)
@verbosity_option(logger=logger)
def list(verbose) -> None:
    """Lists configuration files installed."""
    entry_points = importlib.metadata.entry_points().select(
        group="ptbench.config"
    )
    entry_point_dict = {k.name: k for k in entry_points}

    # all modules with configuration resources
    modules = {k.module.rsplit(".", 1)[0] for k in entry_point_dict.values()}
    keep_modules: set[str] = set()
    for k in sorted(modules):
        if k not in keep_modules and not any(
            k.startswith(element) for element in keep_modules
        ):
            keep_modules.add(k)
    modules = keep_modules

    # sort data entries by originating module
    entry_points_by_module: dict[str, dict[str, typing.Any]] = {}
    for k in modules:
        entry_points_by_module[k] = {}
        for name, ep in entry_point_dict.items():
            if ep.module.startswith(k):
                entry_points_by_module[k][name] = ep

    for config_type in sorted(entry_points_by_module):
        # calculates the longest config name so we offset the printing
        longest_name_length = max(
            len(k) for k in entry_points_by_module[config_type].keys()
        )

        # set-up printing options
        print_string = "  %%-%ds   %%s" % (longest_name_length,)
        # 79 - 4 spaces = 75 (see string above)
        description_leftover = 75 - longest_name_length

        print(f"module: {config_type}")
        for name in sorted(entry_points_by_module[config_type]):
            ep = entry_point_dict[name]

            if verbose >= 1:
                module = ep.load()
                doc = inspect.getdoc(module)
                if doc is not None:
                    summary = doc.split("\n\n")[0]
                else:
                    summary = "<DOCSTRING NOT AVAILABLE>"
            else:
                summary = ""

            summary = (
                (summary[: (description_leftover - 3)] + "...")
                if len(summary) > (description_leftover - 3)
                else summary
            )

            print(print_string % (name, summary))


@config.command(
    epilog="""Examples:

\b
  1. Describes the Montgomery dataset configuration:

     .. code:: sh

        ptbench config describe montgomery


\b
  2. Describes the Montgomery dataset configuration and lists its
     contents:

     .. code:: sh

        ptbench config describe montgomery -v

"""
)
@click.argument(
    "name",
    required=True,
    nargs=-1,
)
@verbosity_option(logger=logger)
def describe(name, verbose) -> None:
    """Describes a specific configuration file."""
    entry_points = importlib.metadata.entry_points().select(
        group="ptbench.config"
    )
    entry_point_dict = {k.name: k for k in entry_points}

    for k in name:
        if k not in entry_point_dict:
            logger.error("Cannot find configuration resource '%s'", k)
            continue
        ep = entry_point_dict[k]
        print(f"Configuration: {ep.name}")
        print(f"Python Module: {ep.module}")
        print("")
        mod = ep.load()

        if verbose >= 1:
            fname = inspect.getfile(mod)
            print("Contents:")
            with open(fname) as f:
                print(f.read())
        else:  # only output documentation
            print("Documentation:")
            print(inspect.getdoc(mod))


@config.command(
    epilog="""Examples:

\b
  1. Makes a copy of one of the stock configuration files locally, so it can be
     adapted:

     .. code:: sh

        $ ptbench config copy montgomery -vvv newdataset.py

"""
)
@click.argument(
    "source",
    required=True,
    nargs=1,
)
@click.argument(
    "destination",
    required=True,
    nargs=1,
)
@verbosity_option(logger=logger, expose_value=False)
def copy(source, destination) -> None:
    """Copy a specific configuration resource so it can be modified locally."""
    import shutil

    entry_points = importlib.metadata.entry_points().select(
        group="ptbench.config"
    )
    entry_point_dict = {k.name: k for k in entry_points}

    if source not in entry_point_dict:
        logger.error("Cannot find configuration resource '%s'", source)
        return

    ep = entry_point_dict[source]
    mod = ep.load()
    src_name = inspect.getfile(mod)
    logger.info(f"cp {src_name} -> {destination}")
    shutil.copyfile(src_name, destination)