Skip to content

edspdf.cli

Cli

Bases: Typer

Custom Typer object that: - validates a command parameters before executing it - accepts a configuration file describing the parameters - automatically instantiates parameters given a dictionary when type hinted

Source code in edspdf/cli.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
class Cli(Typer):
    """
    Custom Typer object that:
    - validates a command parameters before executing it
    - accepts a configuration file describing the parameters
    - automatically instantiates parameters given a dictionary when type hinted
    """

    def command(  # noqa
        self,
        name,
        *,
        cls: Optional[Type[TyperCommand]] = None,
        context_settings: Optional[Dict[Any, Any]] = None,
        help: Optional[str] = None,
        epilog: Optional[str] = None,
        short_help: Optional[str] = None,
        options_metavar: str = "[OPTIONS]",
        add_help_option: bool = True,
        no_args_is_help: bool = False,
        hidden: bool = False,
        deprecated: bool = False,
        # Rich settings
        rich_help_panel: Union[str, None] = Default(None),
    ) -> Callable[[CommandFunctionType], CommandFunctionType]:
        typer_command = super().command(
            name=name,
            cls=cls,
            help=help,
            epilog=epilog,
            short_help=short_help,
            options_metavar=options_metavar,
            add_help_option=add_help_option,
            no_args_is_help=no_args_is_help,
            hidden=hidden,
            deprecated=deprecated,
            rich_help_panel=rich_help_panel,
            context_settings={
                **(context_settings or {}),
                "ignore_unknown_options": True,
                "allow_extra_args": True,
            },
        )

        def wrapper(fn):
            validated = validate_arguments(fn)

            @typer_command
            def command(ctx: Context, config: Optional[Path] = None):
                if config is not None:
                    config = Config.from_str(Path(config).read_text(), resolve=False)
                else:
                    config = Config({name: {}})
                for k, v in parse_overrides(ctx.args).items():
                    if "." not in k:
                        parts = (name, k)
                    else:
                        parts = k.split(".")
                        if (
                            parts[0] in validated.model.__fields__
                            and not parts[0] in config
                        ):
                            parts = (name, *parts)
                    current = config
                    if parts[0] not in current:
                        raise Exception(
                            f"{k} does not match any existing section in config"
                        )
                    for part in parts[:-1]:
                        current = current.setdefault(part, Config())
                    current[parts[-1]] = v
                try:
                    default_seed = validated.model.__fields__.get("seed")
                    seed = config.get(name, {}).get("seed", default_seed)
                    if seed is not None:
                        set_seed(seed)

                    config = config.resolve()

                    return validated(**config.get(name, {}))
                except ValidationError as e:
                    print("\x1b[{}m{}\x1b[0m".format("38;5;1", "Validation error"))
                    print(str(e))
                    sys.exit(1)

            return validated

        return wrapper