Call functions based on dynamic list from the user

I have a report generating API in my django app which takes name of reports as input from the user, user can multiselect different reports from the dropdown

    reports = request.GET['reports'].split(',')

example:

    print("selected reports", reports)

    >>seleceted reports ['AS', 'DD', 'IS', 'LM']

now this reports can vary according to the user and I have a dictionary of all the functions to create these reports

master = {
            'AS': audit_summary(),
            'AH': audit_health(),
            'MS': missing_scan(),
            'IS': individual(),
            'LM': loc_mis(),
            'MAS': missing_assets(),
            'DD' : dump(),
              }

How can I call the functions from the dictionary based on the dynamic list I get from user?

for i in selected_reports:
    masters[i]   # this will call the function

assuming the variable selected_reports contains the list

I think I get what you're trying to do. You could change the master list to be a dictionary of functions instead of function results like this. I'm not 100% sure of the syntax at the end but I know it's possible.

master = {
            'AS': audit_summary,
            'AH': audit_health,
            'MS': missing_scan,
            'IS': individual,
            'LM': loc_mis,
            'MAS': missing_assets,
            'DD' : dump,
              }

def callAllTheFuncts(data):
    for item in data:
        master[item]()

I'm suggesting creating the dictionary so it includes the callables and arguments separately. This prevents the unwanted calls. Here is a simplified example that you can modify to your needs:

data = {
    'a': {'function': print, 'parameters': ['a']},
    'b': {'function': print, 'parameters': ['b']},
    'c': {'function': print, 'parameters': ['c']}
}


def call_funcs():
    for _, v in data.items():
        v['function'](*v['parameters'])


call_funcs()


# Output:
# a
# b
# c

Of course you can use dictionary as argument instead of a list, but just remember to replace * with a ** for key-word arguments.

With your parameters the source dictionary would look like this:

master = {
    'AS': {'function': Summary, 'parameters': [df, description_dict, emp]},
    'AH': {'function': lastSeenMissingAsset, 'parameters': [df, emp, description_dict]},
    'MS': {'function': missingScans, 'parameters': [df, emp]},
    'IS': {'function': individualScans, 'parameters': [df]},
    'LM': {'function': tagMismatch, 'parameters': [df, emp]},
    'MAS': {'function': missingAssets, 'parameters': [df, emp, description_dict, deployed_df]},
    'MAU': {'function': missingAudit, 'parameters': [df, pd.to_datetime(tt).date()]},
    'DD': {'function': dumpData, 'parameters': [df]}
}

It's not clear to me from your post whether you intend for the report contents to be hardcoded in master, or whether you want the report functions to be only called on-demand. Your code hardcodes the report values, but the text you've written suggests to me you want the functions to be called on-demand.

On-demand is more efficient - assuming the contents vary per-user/request, so I've assumed that approach.

If you intend for the reports to be generated only once though, rather than per-request, then the solution would change.

You can use functools.partial to create a callable object that invokes your report-generating functions. This function enables the creation of "partial" function/method invocations that can be called later with extra args or kwargs added (sometimes this is useful). But it can also be used to create a callable that requires no extra args when invoked.

From the official docs:

Return a new partial object which when called will behave like func called with the positional arguments args and keyword arguments keywords. If more arguments are supplied to the call, they are appended to args. If additional keyword arguments are supplied, they extend and override keywords.

from functools import partial

report_generators = dict(
    AS = partial(Summary, df, description_dict, emp),
    AH = partial(lastSeenMissingAsset, df, emp, description_dict),
    MS = partial(missingScans, df, emp),
    IS = partial(individualScans, df),
    LM = partial(tagMismatch, df, emp),
    MAS = partial(missingAssets, df, emp, description_dict, deployed_df),
    MAU = partial(missingAudit, df, partial(pd.to_datetime(tt).date, )),
    DD = partial(dumpData, df),
)

def my_route(self):
    report_ids = request.GET['reports'].split(',')
    # Generate as a flat list.
    report_contents = [report_generators[report_id]() for report_id in report_ids]
    # ..or generate as a dict, keyed by report id.
    report_contents = {report_id:report_generators[report_id]() for report_id in report_ids}
    # Now do stuff with the contents :)

As stated in all comments, the problem is that all your functions (Summary, etc.) are called once when creating the master dictionary. Then you iterate over the selected reports, thus calling some functions a second time.

The solution is indeed to modify master and use partial:

master = {
    'AS': partial(Summary, df, description_dict, emp),
    'AH': partial(lastSeenMissingAsset, df, emp, description_dict),
    'MS': partial(missingScans, df, emp),
    'IS': partial(individualScans, df),
    'LM': partial(tagMismatch, df, emp),
    'MAS': partial(missingAssets, df, emp, description_dict, deployed_df),
    'MAU': partial(missingAudit, df, pd.to_datetime(tt).date()),
    'DD': partial(dumpData, df)
}

Then when you iterate over master:

for i in reports:
    final[i] = master[i]()  # notice the () --> master[i] is a Callable

You just need to store your function and the corresponding arguments separated, so you only join then and call the function when you want to.

For example:

master = {
  'AS': (Summary, (df, description_dict, emp)),
  'AH': (lastSeenMissingAsset, (df, emp, description_dict)),
  'MS': (missingScans, (df, emp)),
  'IS': (individualScans, (df,)),
  'LM': (tagMismatch, (df, emp)),
  'MAS': (missingAssets, (df, emp, description_dict, deployed_df)),
  'MAU': (missingAudit, (df, pd.to_datetime(tt).date())),
  'DD': (dumpData, (df,))
}

final={}
print("selected reports", reports)
for i in reports:
  fn, args = master[i]
  final[i] = fn(*args)
print("final", final)

With lambda, called only when used:

master = {
    'AS': lambda: Summary(df, description_dict, emp),
    'AH': lambda: lastSeenMissingAsset(df, emp, description_dict),
    ...
}

...
    final[i] = master[i]()

The issue is in how your functions are referenced in your master dictionary.

The way you have this defined, you are executing the functions based on the state of the various params when the file is initialized. Instead what you need to have is a reference to the function as the value in your dictionary. Example:

master = {
        'AS': Summary,
        'AH': lastSeenMissingAsset,
        'MS': missingScans,
        'IS': individualScans,
        'LM': tagMismatch,
        'MAS': missingAssets,
        'MAU': missingAudit,
        'DD': dumpData
}                 

and pass your arguments when you pass through the iteration. Like this:

final={}
    print("selected reports", reports)
    for i in reports:
        final[i] = master[i](**kwargs)
    print("final", final)

where **kwargs defines the arguments you need to pass to the function when you execute it.

Note: a more pythonic naming is using lower case function names and snake case (this_is_my_function)

Back to Top