Search code examples
pythonlogicconfigdispatch

Pythonic way to call different methods for each setting in a configuration screen


I am currently designing a configuration screen for my application. There are about 50 configuration parameters, which I have described in a CSV file, and read in Python.

Currently the fields for each parameter row are:

  • csv_row_count
  • cat_name
  • attr_name
  • disp_str
  • unit
  • data_type
  • min_val
  • max_val
  • def_val
  • incr
  • cur_val
  • desc

So a few examples for a parameter could be: Image showing 3 parameter rows in CSV file

Every single parameter is then drawn in my application as UI for the user to interact with, and looks like this: GUI for user to interact with, filled with information from CSV

The csv_row_count is later used as an idx for looping. If I ever want to add another parameter to be configured, I would have to use a unique one and add the parameter to the bottom of the CSV.

I currently have a class which stores all the information into attributes and can be stored back to the CSV after changes by the user.

Now, the problem I am having is that I need to react to the changes the user makes in the configuration screen (uniquely for every single parameter).

So my idea was to create an ConfigEventHandler class, that has an Event for when the parameter is created (on boot of the application, to draw it to the UI for example) and an Event which is fired when the value has been changed (to make changes to the value in the UI, and also react correspondingly for this specific setting that has been changed).

That class looks like this:

from axel import Event
import logging as log

class ConfigEventHandler:
    def __init__(self):
        # Make an Event for when a config row is created (usually only at boot of application)
        self.on_create_ev = Event()
        self.on_create_ev += self._on_create_event

        # Make an Event for when a config row has changed. Fire-ing of this event will be after a change in the attribute.
        self.on_change_ev = Event()
        self.on_change_ev += self._on_value_change_event

    def _on_create_event(self, idx):
        pass
        # log.info("Event OnCreate fired for IDX " + str(idx))

    def _on_value_change_event(self, idx, user_dict):
        log.info("Event On Value Change fired for IDX " + str(idx))
        log.info("With user dict: " + str(user_dict))

And I fire these events on creation of the class and when the values change. The only way for this events to identify which value has been changed, is by the idx value which correlates to the csv_row_count attribute as described in the CSV.

This would mean that if I want to react to each parameter being changed, I would need to fill the _on_value_change_event method with a lot of if-statements, e.g.:

if idx == 0:
    react_to_param0()
elif idx == 1:
    react_to_param1()
etc...

I feel like this is really bad practice (in any program language), and would easily break if someone screwed around with the idx numbering in the CSV file (E.G. me in a few months, or the person that will take over my project).

So my question is, what would be a good alternative for fixing this relationship issue in a way that is more clean than a lot of if-statements and a relationship between a list idx and CSV index to make it more future proof, and keep the code readable?

I have thought of making a seperate class instance for each congifurable parameter, but don't really see how that would break the 'fragile' seeming relation between the csv_row_count/idx and the unique functions that react to changes to one specific parameter.


Solution

  • Doing things in a "pythonic" way often involves using dictionaries, one of the language's primary data-structures which have been highly optimized for speed and minimal memory usage.

    With that in mind, the first thing I'd do is to get the configuration parameters in the CSV file into one. Since there's one per row of the CSV file, a logical thing to do would be to use a csv.DictReader to read them since it returns each row as a dictionary of values. You can easily build a dictionary-of-dictionaries out of those by using the unique csv_row_count (aka idx) field of each one as the key for upper-level dict.

    Here's what I mean:

    import csv
    from pprint import pprint
    
    CONFIG_FILEPATH = 'parameters.csv'
    
    config_params = {}
    with open(CONFIG_FILEPATH, 'r', newline='') as configfile:
        reader = csv.DictReader(configfile, escapechar='\\')
        csv_row_count = reader.fieldnames[0]  # First field is identifier.
        for row in reader:
            idx = int(row.pop(csv_row_count))  # Remove and convert to integer.
            config_params[idx] = row
    
    print('config_params =')
    pprint(config_params, sort_dicts=False)
    

    So if your CSV file contained the rows like this:

    csv_row_count,cat_name,attr_name,disp_str,unit,data_type,min_val,max_val,def_val,incr,cur_val,desc
    15,LEDS,led_gen_use_als,Automatic LED Brightness,-,bool,0.0,1.0,TRUE,1.0,TRUE,Do you want activate automatic brightness control for all the LED's?
    16,LEDS,led_gen_als_led_modifier,LED Brightness Modifier,-,float,0.1,1.0,1,0.05,1,The modifier for LED brightness. Used as static LED brightness value when 'led_gen_use_als' == false.
    17,LEDS,led_gen_launch_show,Enable launch control LEDs,-,bool,0.0,1.0,TRUE,1.0,TRUE,Show when launch control has been activated\, leds will blink when at launch RPM
    

    Reading it with the above code would result in the following dictionary-of-dictionaries being built:

    config_params =
    {15: {'cat_name': 'LEDS',
          'attr_name': 'led_gen_use_als',
          'disp_str': 'Automatic LED Brightness',
          'unit': '-',
          'data_type': 'bool',
          'min_val': '0.0',
          'max_val': '1.0',
          'def_val': 'TRUE',
          'incr': '1.0',
          'cur_val': 'TRUE',
          'desc': 'Do you want activate automatic brightness control for all the '
                  "LED's?"},
     16: {'cat_name': 'LEDS',
          'attr_name': 'led_gen_als_led_modifier',
          'disp_str': 'LED Brightness Modifier',
          'unit': '-',
          'data_type': 'float',
          'min_val': '0.1',
          'max_val': '1.0',
          'def_val': '1',
          'incr': '0.05',
          'cur_val': '1',
          'desc': 'The modifier for LED brightness. Used as static LED brightness '
                  "value when 'led_gen_use_als' == false."},
     17: {'cat_name': 'LEDS',
          'attr_name': 'led_gen_launch_show',
          'disp_str': 'Enable launch control LEDs',
          'unit': '-',
          'data_type': 'bool',
          'min_val': '0.0',
          'max_val': '1.0',
          'def_val': 'TRUE',
          'incr': '1.0',
          'cur_val': 'TRUE',
          'desc': 'Show when launch control has been activated, leds will blink '
                  'when at launch RPM'}}
    

    The Dispatching of the Functions

    Now, as far as reacting to each parameter change goes, one way to do it would be by using a function decorator to define when the function of the decorator it is applied to will get called based on the value of its first argument. This will eliminate the need for a long series of if-statements in your _on_value_change_event() method — because it can be replaced with a single call to the named decorated function.

    I got the idea for implementing a decorator to achieve this from the article Learn About Python Decorators by Writing a Function Dispatcher, although I have implemented it in a slightly different manner using a class (instead of a function with nested-functions), which I think is a little "cleaner". Also note how the decorator itself uses an internal dictionary named registry to do the store information about and to do the actual dispatching — another "pythonic" way to do things.

    Again, here's what I mean:

    class dispatch_on_value:
        """ Value-dispatch function decorator.
    
        Transforms a function into a value-dispatch function, which can have
        different behaviors based on the value of its first argument.
    
        See: http://hackwrite.com/posts/learn-about-python-decorators-by-writing-a-function-dispatcher
        """
        def __init__(self, func):
            self.func = func
            self.registry = {}
    
        def dispatch(self, value):
            try:
                return self.registry[value]
            except KeyError:
                return self.func
    
        def register(self, value, func=None):
            if func is None:
                return lambda f: self.register(value, f)
            self.registry[value] = func
            return func
    
        def __call__(self, *args, **kw):
            return self.dispatch(args[0])(*args, **kw)
    
    @dispatch_on_value
    def react_to_param(idx):  # Default function when no match.
        print(f'In default react_to_param function for idx == {idx}.')
    
    @react_to_param.register(15)
    def _(idx):
        print('In react_to_param function registered for idx 15')
    
    @react_to_param.register(16)
    def _(idx):
        print('In react_to_param function registered for idx 16')
    
    @react_to_param.register(17)
    def _(idx):
        print('In react_to_param function registered for idx 17')
    
    # Test dispatching.
    for idx in range(14, 19):
        react_to_param(idx)  # Only this line would go in your `_on_value_change_event()` method.
    
    

    Here's the output produced that shows it works. Note that there wasn't a function registered for every possible value, in which the the default function gets called.

    In default react_to_param function for idx == 14.
    In react_to_param function registered for idx 15
    In react_to_param function registered for idx 16
    In react_to_param function registered for idx 17
    In default react_to_param function for idx == 18.