Skip to content

Request: Create a settable.setter_cb callback option #1026

@Script-Nomad

Description

@Script-Nomad

I'm writing a somewhat complex application with Cmd2 and I'm loving the ease at which I can add new commands, functionality, and such to it. One hang-up I'm having is with the Settable class, more specifically that it lacks the ability to allow the user to define how a settable parameter is set.

I looked at the code for the onchange_cb() method which allows the programmer to define actions after the settable parameter is given a new value, but it does not allow the user to change the behavior of how the parameter is set. This would be extremely useful, especially in my case where I am trying to make it possible for the user to set the values for a complex object, in this case, a dictionary. In my particular use-case, I created a form-like method for the user to input values into the dictionary which would be validated before being passed back to the settable dictionary.

I know I can get around this by simply writing a dedicated function to set this value as a property of my cmd2 app class, but it feels dirty and unnecessary.

My use case

The way I imagine this working at the user-level is something like this:

class MyApp(cmd2.Cmd):
     def __init__(self, my_params, blahblah, *args, **kwargs)
         ... truncated ...
         self.add_settable(cmd2.Settable('dict_param', bool, setter_cb=self._set_dict_param,
                                        description='Input true to change the dict_param'))
         ... truncated ...
     def _set_dict_param(self, param, old, new):
         ... code for setting the dictionary values ...
         return new_dict_param

At the user-layer, setting the value looks something like this:

prompt> set dict_param
              dict_param = { 'foo': 'bar', 'bin': 'baz }
prompt> set dict_param true
               Set your dict_param values here. Leave empty to end loop:
    > foo=baz
    > bin=bar
    > 
dict_param: { 'foo': 'baz', 'bin': 'bar' }

I have a very particular use-case which is a lot more complex than this example, but you get the idea...

What I tried so far on my own:

I took the liberty of modifying the cmd2 source a little bit to see how practical this would be, and it seems quite simple to me, but I think I'm missing something.

I modified the Settable class in cmd2/utils.py like so:

class Settable:
    """Used to configure a cmd2 instance member to be settable via the set command in the CLI"""
    def __init__(self, name: str, val_type: Callable, description: str, *,
                 onchange_cb: Callable[[str, Any, Any], Any] = None,
                 setter_cb: Callable[[str, Any, Any], Any] = None,
                 ... snip ...
                """ Inside the docstring
                 ... snip ...
                :param setter_cb: : optional function or method to call which defines how the value of this settable
                            is altered by the set command. (e.g. setter_cb=self.param_setter)

                            Cmd.do_set() passes the following 3 arguments to setter_cb:
                                param_name: str - name of the parameter to change
                                old_value: Any - the value before being changed
                                new_value: Any - the value sent by the caller
                            :return : Any - the value returned by the callback function which updates
                                      the settable value
                ... snip ...
               """
              self.setter_cb = setter_cb

Then I modified cmd2/cmd2.py's Cmd.do_set() method like so:

def do_set(self, args: argparse.Namespace) -> None:
    ... snip ...
    if args.value:
                args.value = utils.strip_quotes(args.value)

                # Try to update the settable's value
                try:
                    orig_value = getattr(self, args.param)
                    if getattr(settable, setter_cb):
                        setattr(self, args.param, settable.setter_cb(args.param, orig_value, args.value))
                    else:
                        setattr(self, args.param, settable.val_type(args.value))
                    new_value = getattr(self, args.param)
                # noinspection PyBroadException
                except Exception as e:
                    err_msg = "Error setting {}: {}".format(args.param, e)
                    self.perror(err_msg)
                    return
    ... snip ....

In effect, my goal was to emulate the same parameters and behavior as the onchange_cb method, except that instead of performing actions after the parameter was set, it would expect a return value which would then set the settable parameter's value.

I would be happy to submit a PR for this, but this change always results in the following error:
Error setting dict_param: name 'setter_cb' is not defined

Maybe I'm missing something obvious, or python is importing from some hidden version of the library that I haven't changed and I'm just too dumb to figure out how to bypass that.

Whatever the case, I figure this would be best left up to the maintainers of Cmd2. If this change seems useful/

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions