question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Request: Create a settable.setter_cb callback option

See original GitHub issue

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/

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
kmvanbruntcommented, Nov 26, 2020

The Settable class has a member called val_type which is defined as:

val_type: callable used to cast the string value from the command line into its proper type and
          even validate its value. The val_type function should raise an exception if it fails.
          This exception will be caught and printed by Cmd.do_set().

I believe this already does what you are looking for. Just set val_type to your function which prompts the user and return the dictionary when finished. Setting choices to true/false will give you tab completion on the set command.

self.add_settable(cmd2.Settable('dict_param', self._set_dict_param,  'Input true to change the dict_param',
                                 choices=['true', 'false']))

Please view Settable.__init__() for full documentation of all parameters.

0reactions
True-Demoncommented, Dec 6, 2020

For the sake of those who run into this in the future, I wanted settable to be able to accept a dictionary, but one that had very specific value requirements. in order to set this dictionary values, I needed to make sure to hold the user’s hand and prevent them from putting in bad/invalid values for the dict.

In this case, this dictionary was a dict with the CVSS (Common Vulnerability Scoring System) thresholds for measuring vulnerabilities in applications, but this could apply to any kind of complex, mutable data type such as records via NamedTuples or just simple Lists.

Here is what my implementation looked like so others can take advantage of this hidden gem:

class MyApp(cmd2.Cmd):
    def __init__(self, *args):
        # ... your init stuff ...
        self.add_settable(cmd2.Settable('cvss_thresholds', val_type=self._set_cvss_thresholds, choices=('true', 'false', 'default'),
                                        description='Input true to change the minimum risk thresholds for CVSS scores'))

    def _set_cvss_thresholds(self, arg):
        if arg.lower() == 'default':
            return self.config.get('cvss_thresholds')
        def verify_threshold(value, last, thresholds):
            try:
                value = float(value)
            except ValueError:
                if len(value) == 0:
                    return -1
            if isinstance(value, float) and 0.0 <= value <= 10.0:
                i = thresholds.index(k)
                if i == 0 and value >= 0.0:
                    return value
                elif value > last:
                    return value
                else:
                    return False
            else:
                return False

        self.poutput("Set the minimum value for each threshold. An empty value removes the threshold")
        thresholds = ('info', 'low', 'medium', 'high', 'critical')
        # Use deepcopy to create an ACTUAL copy of the dictionary
        new_thresholds = copy.deepcopy(self.cvss_thresholds)
        print(new_thresholds)
        last = 0.0
        for k in thresholds:
            try:
                while True:
                    v = verify_threshold(input(f"{self.continuation_prompt}{k}: "), last, thresholds)
                    if isinstance(v, bool) and v == False:
                        self.perror("Invalid value try again.")
                        continue
                    elif v != -1:
                        new_thresholds[k] = v
                        self.poutput(f"{k} set to: {v}")
                        last = v
                        break
                    else:
                        self.poutput(f"Removing: {k}")
                        del new_thresholds[k]
                        break
            except KeyboardInterrupt:
                return self.cvss_thresholds
        return new_thresholds

Example Run:

MyAPP ≫  set cvss_thresholds true
Set the minimum value for each threshold. An empty value removes the threshold
{'high': 6.0, 'medium': 3.0, 'low': 0.1, 'info': 0.0}
MyAPP ≫  info: 0.0
info set to: 0.0
MyAPP ≫  low: 0.1
low set to: 0.1
MyAPP ≫  medium: 4.0
medium set to: 4.0
MyAPP ≫  high: 6.0
high set to: 6.0
MyAPP ≫  critical: 9.0
critical set to: 9.0
cvss_thresholds - was: {'high': 6.0, 'medium': 3.0, 'low': 0.1, 'info': 0.0}
now: {'high': 6.0, 'medium': 4.0, 'low': 0.1, 'info': 0.0, 'critical': 9.0}

Example perror output:

Note: CVSS values are only validi between 0.0 and 10.0

MyAPP ≫  set cvss_thresholds true
Set the minimum value for each threshold. An empty value removes the threshold
{'high': 6.0, 'medium': 3.0, 'low': 0.1, 'info': 0.0}
MyAPP ≫  info: -100
Invalid value try again.
MyAPP ≫  info: 10.1
Invalid value try again.
MyAPP ≫  info:                                  # Empty value
Removing: info

Example set to default

MyAPP ≫  set cvss_thresholds default
cvss_thresholds - was: {'high': 6.0, 'medium': 4.0, 'low': 0.1, 'info': 0.0, 'critical': 9.0}
now: {'high': 6.0, 'medium': 3.0, 'low': 0.1, 'info': 0.0}
Read more comments on GitHub >

github_iconTop Results From Across the Web

Add a scheduled callback option to a script
This is how to add a schedule callback option to a script using date ... Set Includes Time to Yes. ... Create a...
Read more >
Enabling customer callback - Zendesk help
Hi! Is there a way to set up the call back feature, enabling the customer to request a certain time to be called...
Read more >
Give Customers the Option to Request a Callback
This example shows how to set up an agent-first callback in your inbound contact flow. The contact flow defines the interactive voice response...
Read more >
Set up queued callback - Amazon Connect
Set this up so that contacts waiting for a call are routed to agents. Create a flow for queued callbacks. You offer the...
Read more >
Callback User's Guide - Genesys Documentation
Set the Prefix Dial Out Option. To make sure that the system will be able to call, configure the _prefix_dial_out option in your...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found