mirror of
https://github.com/revng/revng
synced 2026-06-21 14:07:57 +00:00
e8d6e48a1f
The `--format` option relied on `click.Context.get_parameter_source` to determine if the default value for the `--format` parameter was passed. A bug in click caused this to behave "correctly", click 8.4.0 fixed the bug. Change the logic by using `None` as the default value so that it can be checked without using the parameter source.
252 lines
8.0 KiB
Python
252 lines
8.0 KiB
Python
#
|
|
# This file is distributed under the MIT License. See LICENSE.md for details.
|
|
#
|
|
|
|
from enum import Enum
|
|
from pathlib import Path
|
|
from typing import Generator, cast
|
|
|
|
import click
|
|
from click_option_group import GroupedOption, OptionGroup
|
|
|
|
from revng.pypeline.analysis import Analysis, AnalysisList
|
|
from revng.pypeline.cli.context import ClickContext
|
|
from revng.pypeline.cli.utils import EagerParsedPath, detect_autocomplete, normalize_flag
|
|
from revng.pypeline.container import ContainerFormat
|
|
from revng.pypeline.pipeline import AnalysisBinding, Pipeline
|
|
from revng.pypeline.pipeline_node import PipelineNode
|
|
from revng.pypeline.runner_context import RunnerContext
|
|
from revng.pypeline.task.pipe import Pipe
|
|
from revng.pypeline.utils.registry import get_registry
|
|
|
|
# Options that are common to multiple commands
|
|
project_id_option = click.option(
|
|
"--project-id",
|
|
type=str,
|
|
help="Project id to use for the storage provider.",
|
|
envvar="PYPELINE_PROJECT_ID",
|
|
show_default=True,
|
|
)
|
|
|
|
list_objects_option = click.option(
|
|
"--list",
|
|
type=bool,
|
|
is_flag=True,
|
|
default=False,
|
|
help="List the available objects for each argument.",
|
|
)
|
|
|
|
token_option = click.option(
|
|
"--token",
|
|
type=str,
|
|
required=False,
|
|
help="The token to pass to the storage provider.",
|
|
)
|
|
|
|
|
|
_FORMAT_VARIABLE = "container_format"
|
|
|
|
|
|
def handle_format_option(ctx, param, value):
|
|
"""
|
|
Handles mutual exclusivity, converts strings to Enums,
|
|
and manages precedence between flags and defaults.
|
|
"""
|
|
# Handle shortcuts the value is boolean here
|
|
if param.name in [f.value for f in ContainerFormat]:
|
|
if not value:
|
|
return None
|
|
|
|
if ctx.params.get(_FORMAT_VARIABLE) is not None:
|
|
raise click.BadOptionUsage(
|
|
param.name, f"Mutually exclusive: --{param.name} cannot be used with other formats."
|
|
)
|
|
|
|
ctx.params[_FORMAT_VARIABLE] = ContainerFormat(param.name)
|
|
return None
|
|
|
|
# Handle --format, value is a string passed from click.Choice or default
|
|
|
|
# Check if a shortcut already populated the format
|
|
existing_format = ctx.params.get(_FORMAT_VARIABLE)
|
|
if existing_format is not None:
|
|
# If the user EXPLICITLY typed --format AND used a flag -> Error
|
|
if value is not None:
|
|
raise click.BadOptionUsage(
|
|
param.name,
|
|
"Mutually exclusive: Cannot specify "
|
|
f"--format when --{existing_format.value} is used.",
|
|
)
|
|
# If --format is just running its default 'yaml', let the Flag win
|
|
return existing_format
|
|
|
|
if value is not None:
|
|
# If no flag was used, convert the string value to Enum and return
|
|
return ContainerFormat(value)
|
|
else:
|
|
return ContainerFormat.YAML
|
|
|
|
|
|
def container_format_options(func):
|
|
func = click.option(
|
|
"--format",
|
|
_FORMAT_VARIABLE,
|
|
type=click.Choice([x.value for x in ContainerFormat]),
|
|
callback=handle_format_option,
|
|
help=(
|
|
"Format to use for the output container, either on stdout or in "
|
|
"the result path. [default: yaml]"
|
|
),
|
|
)(func)
|
|
for member in ContainerFormat:
|
|
func = click.option(
|
|
f"--{member.value}",
|
|
is_flag=True,
|
|
expose_value=False,
|
|
help=f"Shortcut for --format={member.value}.",
|
|
callback=handle_format_option,
|
|
)(func)
|
|
return func
|
|
|
|
|
|
def _parse_debug_option(path: str, ctx: ClickContext):
|
|
return RunnerContext(True, ctx.obj.pipebox.argv_hook, Path(path))
|
|
|
|
|
|
debug_option = click.option(
|
|
"--debug",
|
|
"runner_context",
|
|
type=EagerParsedPath(
|
|
name="runner_context",
|
|
parser=_parse_debug_option,
|
|
default=RunnerContext(),
|
|
dir_okay=True,
|
|
file_okay=False,
|
|
exists=False,
|
|
),
|
|
help=(
|
|
"""
|
|
Run the command in debug mode with the specified directory (it will be
|
|
created if missing). Where possible the pipes and analyses will be run
|
|
as subcommands via `run-pipe` and `run-analysis` with input and output
|
|
files in subdirectories in the specified directory.
|
|
"""
|
|
),
|
|
default=EagerParsedPath.DEFAULT,
|
|
show_default=False,
|
|
)
|
|
|
|
|
|
def _show_full_help(ctx: ClickContext, param, value: bool):
|
|
if not value:
|
|
return
|
|
|
|
ctx.obj.show_hidden = True
|
|
click.echo(ctx.get_help())
|
|
ctx.exit()
|
|
|
|
|
|
full_help = click.option(
|
|
"--help-full",
|
|
is_flag=True,
|
|
callback=_show_full_help,
|
|
is_eager=True,
|
|
expose_value=False,
|
|
help="Show help with hidden options and exit.",
|
|
)
|
|
|
|
|
|
class _ConfigurationOptionType(click.ParamType):
|
|
def __init__(self, key: type[Pipe] | type[Analysis]):
|
|
self.name = f"{normalize_flag(key.name)}-configuration"
|
|
self.key = key
|
|
|
|
def convert(self, value, param, ctx: ClickContext): # type: ignore
|
|
if issubclass(self.key, Analysis):
|
|
for binding in ctx.obj.pipeline.analyses.values():
|
|
if isinstance(binding.analysis, self.key):
|
|
ctx.obj.configuration[binding.analysis] = value
|
|
else:
|
|
for node in ctx.obj.pipeline.walk_pipeline():
|
|
if isinstance(node.task, self.key):
|
|
ctx.obj.configuration[node.task] = value
|
|
|
|
|
|
class _HidableOption(GroupedOption):
|
|
@property
|
|
def hidden(self):
|
|
ctx: ClickContext | None = cast(ClickContext | None, click.get_current_context(silent=True))
|
|
if detect_autocomplete(ctx):
|
|
return False
|
|
return False if ctx is None else not ctx.obj.show_hidden
|
|
|
|
@hidden.setter
|
|
def hidden(self, value):
|
|
pass
|
|
|
|
|
|
class AllAnalysesOption(Enum):
|
|
ALL_ANALYSES = 0
|
|
|
|
|
|
ConfigTarget = PipelineNode | AllAnalysesOption | AnalysisBinding | AnalysisList
|
|
ComponentSet = set[type[Pipe] | type[Analysis]]
|
|
|
|
|
|
def _flatten(gen: Generator[ComponentSet, None, None]) -> ComponentSet:
|
|
result: ComponentSet = set()
|
|
for element in gen:
|
|
result.update(element)
|
|
return result
|
|
|
|
|
|
def _get_config_components(pipeline: Pipeline, target: ConfigTarget) -> ComponentSet:
|
|
result: ComponentSet = set()
|
|
if isinstance(target, PipelineNode):
|
|
nodes: list[PipelineNode] = [target]
|
|
while len(nodes) > 0:
|
|
node = nodes.pop(0)
|
|
if isinstance(node.task, Pipe):
|
|
result.add(node.task.__class__)
|
|
nodes.extend(node.predecessors)
|
|
return result
|
|
elif isinstance(target, AnalysisBinding):
|
|
return _get_config_components(pipeline, target.node)
|
|
elif isinstance(target, AnalysisList):
|
|
for analysis_name in target.analyses:
|
|
binding = pipeline.analyses[analysis_name]
|
|
result.add(binding.analysis.__class__)
|
|
result.update(_get_config_components(pipeline, binding.node))
|
|
return result
|
|
elif target is AllAnalysesOption.ALL_ANALYSES:
|
|
for analysis_type in get_registry(Analysis).values(): # type: ignore[type-abstract]
|
|
result.add(analysis_type)
|
|
for analysis_binding in pipeline.analyses.values():
|
|
if isinstance(analysis_binding.analysis, analysis_type):
|
|
result.update(_get_config_components(pipeline, analysis_binding.node))
|
|
return result
|
|
else:
|
|
raise ValueError
|
|
|
|
|
|
def add_pipeline_config_options(pipeline: Pipeline, *targets: ConfigTarget):
|
|
def decorator(func):
|
|
group = OptionGroup("Pipe/Analysis configuration options")
|
|
values = _flatten(_get_config_components(pipeline, t) for t in targets)
|
|
for type_ in sorted(values, key=lambda x: x.name):
|
|
type_string = "pipe" if issubclass(type_, Pipe) else "analysis"
|
|
func = group.option(
|
|
f"--{normalize_flag(type_.name)}-configuration",
|
|
type=_ConfigurationOptionType(type_),
|
|
expose_value=False,
|
|
cls=_HidableOption,
|
|
metavar="CONFIGURATION",
|
|
help=f"Configuration for the {type_.name} {type_string}",
|
|
)(func)
|
|
|
|
func = full_help(func)
|
|
|
|
return func
|
|
|
|
return decorator
|