Skip to content

Commit 25558f9

Browse files
committed
Fix duplicate options warnings
Also turn the warning into an error in the help pages test. fixes #1315
1 parent f8ef5fe commit 25558f9

8 files changed

Lines changed: 154 additions & 80 deletions

File tree

CHANGES/1315.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed warnings about parameters being used multiple times.

src/pulp_cli/generic.py

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ def __init__(
399399
):
400400
self.allowed_with_contexts = allowed_with_contexts
401401
self.needs_plugins = needs_plugins
402+
self.option_processors: list[t.Callable[[click.Context], None]] = []
402403
super().__init__(*args, **kwargs)
403404

404405
def invoke(self, ctx: click.Context) -> t.Any:
@@ -408,6 +409,8 @@ def invoke(self, ctx: click.Context) -> t.Any:
408409
assert pulp_ctx is not None
409410
for plugin_requirement in self.needs_plugins:
410411
pulp_ctx.needs_plugin(plugin_requirement)
412+
for processor in self.option_processors:
413+
processor(ctx)
411414
return super().invoke(ctx)
412415
except PulpException as e:
413416
raise click.ClickException(str(e))
@@ -471,7 +474,9 @@ def list_commands(self, ctx: click.Context) -> list[str]:
471474

472475

473476
def pulp_command(
474-
name: str | None = None, **kwargs: t.Any
477+
name: str | None = None,
478+
cls: t.Type[PulpCommand] = PulpCommand,
479+
**kwargs: t.Any,
475480
) -> t.Callable[[_AnyCallable], PulpCommand]:
476481
"""
477482
Pulp command factory.
@@ -480,17 +485,21 @@ def pulp_command(
480485
`allowed_with_contexts`. It allows rendering the docstring with the values of `ENTITY` and
481486
`ENTITIES` from the closest entity context.
482487
"""
483-
return click.command(name=name, cls=PulpCommand, **kwargs)
488+
return click.command(name=name, cls=cls, **kwargs)
484489

485490

486-
def pulp_group(name: str | None = None, **kwargs: t.Any) -> t.Callable[[_AnyCallable], PulpGroup]:
491+
def pulp_group(
492+
name: str | None = None,
493+
cls: t.Type[PulpGroup] = PulpGroup,
494+
**kwargs: t.Any,
495+
) -> t.Callable[[_AnyCallable], PulpGroup]:
487496
"""
488497
Pulp command group factory.
489498
490499
Creates a click compatible group command that selects subcommands based on
491500
`allowed_with_contexts` and creates `PulpCommand` subcommands by default.
492501
"""
493-
return click.group(name=name, cls=PulpGroup, **kwargs)
502+
return click.group(name=name, cls=cls, **kwargs)
494503

495504

496505
class PulpOption(click.Option):
@@ -631,7 +640,9 @@ def _version_callback(ctx: click.Context, param: click.Parameter, value: int | N
631640
return value
632641

633642

634-
def load_file_wrapper(handler: t.Callable[[click.Context, click.Parameter, str], t.Any]) -> t.Any:
643+
def load_file_wrapper(
644+
handler: t.Callable[[click.Context, click.Parameter, str], t.Any],
645+
) -> t.Any:
635646
"""
636647
A wrapper that is used for chaining or decorating callbacks that manipulate input data.
637648
@@ -799,7 +810,11 @@ def remote_header_callback(
799810
# Decorator common options
800811

801812

802-
def pulp_option(*args: t.Any, **kwargs: t.Any) -> t.Callable[[FC], FC]:
813+
def pulp_option(
814+
*args: t.Any,
815+
cls: t.Type[PulpOption] = PulpOption,
816+
**kwargs: t.Any,
817+
) -> t.Callable[[FC], FC]:
803818
"""
804819
Factory of [`PulpOption`][pulp_cli.generic.PulpOption] objects.
805820
@@ -820,8 +835,72 @@ def pulp_option(*args: t.Any, **kwargs: t.Any) -> t.Callable[[FC], FC]:
820835
)
821836
```
822837
"""
823-
kwargs["cls"] = PulpOption
824-
return click.option(*args, **kwargs)
838+
return click.option(*args, cls=cls, **kwargs)
839+
840+
841+
def option_processor(
842+
*, callback: t.Callable[[click.Context], None]
843+
) -> t.Callable[[PulpCommand], PulpCommand]:
844+
"""
845+
Add a callback i.e. for option processing at a rather generic place before calling the command.
846+
"""
847+
848+
def _inner(cmd: PulpCommand) -> PulpCommand:
849+
cmd.option_processors.append(callback)
850+
return cmd
851+
852+
return _inner
853+
854+
855+
def option_group(
856+
name: str,
857+
options: t.List[str],
858+
/,
859+
callback: t.Callable[[click.Context, t.Dict[str, t.Any]], t.Any] | None = None,
860+
require_all: bool = True,
861+
expose_value: bool = True,
862+
) -> t.Callable[[PulpCommand], PulpCommand]:
863+
def _group_callback(ctx: click.Context) -> None:
864+
"""
865+
Group a list of options into a group represented as a dictionary.
866+
This allows to add a `callback` function for further processing.
867+
`expose_value` allows to hide the value from the command callback.
868+
"""
869+
value = {k: v for k, v in ((k, ctx.params.pop(k, None)) for k in options) if v is not None}
870+
if value:
871+
if require_all and (missing_options := set(options) - set(value.keys())):
872+
raise click.UsageError(
873+
_(
874+
"Illegal usage, please specify all options in the group: {option_list}\n"
875+
"missing: {missing_options}"
876+
).format(
877+
option_list=", ".join(options),
878+
missing_options=", ".join(missing_options),
879+
)
880+
)
881+
882+
if callback is not None:
883+
value = callback(ctx, value)
884+
if expose_value:
885+
ctx.params[name] = value
886+
887+
def _inner(cmd: PulpCommand) -> PulpCommand:
888+
for param in cmd.params:
889+
if param.name in options:
890+
other_options = [o for o in options if o != param.name]
891+
if other_options:
892+
help_text = (
893+
(getattr(param, "help") or "")
894+
+ "\n"
895+
+ _("Option is grouped with {options}.").format(
896+
options=", ".join(other_options)
897+
)
898+
).strip()
899+
setattr(param, "help", help_text)
900+
cmd.option_processors.append(_group_callback)
901+
return cmd
902+
903+
return _inner
825904

826905

827906
def resource_lookup_option(*args: t.Any, **kwargs: t.Any) -> t.Callable[[FC], FC]:
@@ -1013,7 +1092,11 @@ def _option_callback(
10131092
_(
10141093
"The type '{plugin}:{resource_type}' "
10151094
"does not support the '{capability}' capability."
1016-
).format(plugin=plugin, resource_type=resource_type, capability=capability)
1095+
).format(
1096+
plugin=plugin,
1097+
resource_type=resource_type,
1098+
capability=capability,
1099+
)
10171100
)
10181101

10191102
if parent_resource_lookup:
@@ -1044,7 +1127,7 @@ def _multi_option_callback(
10441127
"{plugin_default}{type_default}{multiple_note}"
10451128
).format(
10461129
plugin_form=_("[<plugin>:]") if default_plugin else _("<plugin>:"),
1047-
type_form=_("[<resource_type>:]") if default_type else _("<resource_type>:"),
1130+
type_form=(_("[<resource_type>:]") if default_type else _("<resource_type>:")),
10481131
lookup_key=lookup_key,
10491132
plugin_default=(
10501133
_("'<plugin>' defaults to {plugin}. ").format(plugin=default_plugin)
@@ -1376,7 +1459,9 @@ def _type_callback(ctx: click.Context, param: click.Parameter, value: str | None
13761459
help=_("total number of simultaneous connections"),
13771460
),
13781461
click.option(
1379-
"--rate-limit", type=int_or_empty, help=_("limit download rate in requests per second")
1462+
"--rate-limit",
1463+
type=int_or_empty,
1464+
help=_("limit download rate in requests per second"),
13801465
),
13811466
click.option("--sock-connect-timeout", type=float_or_empty),
13821467
click.option("--sock-read-timeout", type=float_or_empty),

src/pulpcore/cli/ansible/remote.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,6 @@ def remote(ctx: click.Context, pulp_ctx: PulpCLIContext, /, remote_type: str) ->
6767
nested_lookup_options = [remote_lookup_option]
6868
remote_options = [
6969
click.option("--policy", help=_("policy to use when downloading")),
70-
]
71-
collection_options = [
7270
pulp_option(
7371
"--requirements-file",
7472
callback=yaml_callback,
@@ -96,9 +94,8 @@ def remote(ctx: click.Context, pulp_ctx: PulpCLIContext, /, remote_type: str) ->
9694
allowed_with_contexts=collection_context,
9795
),
9896
]
99-
ansible_remote_options = remote_options + collection_options
100-
create_options = common_remote_create_options + remote_options + ansible_remote_options
101-
update_options = common_remote_update_options + remote_options + ansible_remote_options
97+
create_options = common_remote_create_options + remote_options
98+
update_options = common_remote_update_options + remote_options
10299

103100
remote.add_command(list_command(decorators=remote_filter_options))
104101
remote.add_command(show_command(decorators=lookup_options))

src/pulpcore/cli/container/content.py

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@
33

44
import click
55

6-
from pulp_glue.common.context import PluginRequirement, PulpEntityContext
6+
from pulp_glue.common.context import PluginRequirement, PulpContentContext
77
from pulp_glue.container.context import (
88
PulpContainerBlobContext,
99
PulpContainerManifestContext,
1010
PulpContainerTagContext,
1111
)
1212

1313
from pulp_cli.generic import (
14-
GroupOption,
1514
PulpCLIContext,
1615
content_filter_options,
1716
href_option,
1817
label_command,
1918
list_command,
19+
option_group,
2020
pass_pulp_context,
2121
pulp_group,
2222
pulp_option,
@@ -26,14 +26,13 @@
2626
_ = gettext.gettext
2727

2828

29-
def _content_callback(ctx: click.Context, param: click.Parameter, value: t.Any) -> None:
30-
if value is not None:
31-
entity_ctx = ctx.find_object(PulpEntityContext)
29+
def _content_callback(ctx: click.Context, value: t.Dict[str, t.Any]) -> None:
30+
if value:
31+
entity_ctx = ctx.find_object(PulpContentContext)
3232
assert entity_ctx is not None
33-
if isinstance(entity_ctx, PulpContainerTagContext):
34-
entity_ctx.entity = value
35-
else:
36-
entity_ctx.entity = {"digest": value}
33+
if isinstance(entity_ctx, PulpContainerTagContext) and len(value) != 2:
34+
raise click.UsageError(_("Both 'name' and 'digest' are needed to describe a tag."))
35+
entity_ctx.entity = value
3736

3837

3938
@pulp_group()
@@ -64,7 +63,10 @@ def content(ctx: click.Context, pulp_ctx: PulpCLIContext, /, content_type: str)
6463
),
6564
pulp_option("--name", "name", allowed_with_contexts=(PulpContainerTagContext,)),
6665
pulp_option(
67-
"--name-in", "name__in", multiple=True, allowed_with_contexts=(PulpContainerTagContext,)
66+
"--name-in",
67+
"name__in",
68+
multiple=True,
69+
allowed_with_contexts=(PulpContainerTagContext,),
6870
),
6971
pulp_option(
7072
"--digest",
@@ -98,33 +100,21 @@ def content(ctx: click.Context, pulp_ctx: PulpCLIContext, /, content_type: str)
98100
lookup_options = [
99101
pulp_option(
100102
"--digest",
101-
expose_value=False,
102103
help=_("Digest associated with {entity}"),
103-
callback=_content_callback,
104-
allowed_with_contexts=(PulpContainerBlobContext, PulpContainerManifestContext),
105104
),
106-
click.option(
107-
"--digest",
108-
expose_value=False,
109-
help=_("Digest associated with {entity}"),
110-
callback=_content_callback,
111-
allowed_with_contexts=(PulpContainerTagContext,),
112-
group=[
113-
"name",
114-
],
115-
cls=GroupOption,
116-
),
117-
click.option(
105+
pulp_option(
118106
"--name",
119-
expose_value=False,
120107
help=_("Name of {entity}"),
121108
allowed_with_contexts=(PulpContainerTagContext,),
122-
group=[
123-
"digest",
124-
],
125-
cls=GroupOption,
126109
),
127110
href_option,
111+
option_group(
112+
"content",
113+
["name", "digest"],
114+
callback=_content_callback,
115+
require_all=False,
116+
expose_value=False,
117+
),
128118
]
129119

130120
content.add_command(list_command(decorators=list_options))

src/pulpcore/cli/core/content_guard.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,6 @@ def x509(ctx: click.Context, pulp_ctx: PulpCLIContext, /) -> None:
203203

204204

205205
certguard_create_options = common_create_options + [
206-
click.option("--description"),
207206
click.option(
208207
"--ca-certificate",
209208
help=_("a PEM encoded CA certificate or @file containing same"),
@@ -212,14 +211,17 @@ def x509(ctx: click.Context, pulp_ctx: PulpCLIContext, /) -> None:
212211
),
213212
]
214213

215-
certguard_update_options = lookup_options + [
216-
click.option("--description"),
217-
click.option(
218-
"--ca-certificate",
219-
help=_("a PEM encoded CA certificate or @file containing same"),
220-
callback=load_string_callback,
221-
),
222-
]
214+
certguard_update_options = (
215+
lookup_options
216+
+ common_update_options
217+
+ [
218+
click.option(
219+
"--ca-certificate",
220+
help=_("a PEM encoded CA certificate or @file containing same"),
221+
callback=load_string_callback,
222+
),
223+
]
224+
)
223225

224226

225227
x509.add_command(list_command(decorators=filter_options))

0 commit comments

Comments
 (0)