@@ -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
473476def 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
496505class 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
827906def 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 ),
0 commit comments