Search code examples
pythonyamlevalpyyaml

How to execute a boolean logic stored as a string, preferably without eval()?


I have the below YAML file, containing the parsing logic to match a file name:

rules_container = """
file_name:
  rule_1: {module: str, method: startswith, args: [s_]}
  rule_2: {module: str, method: endswith, args: [.log]}
  logic: rule_1 and rule_2
"""

I load it and I get the below mapping:

>>> import yaml
>>> import pprint
>>> d_yml = yaml.safe_load(rules_container)
>>> pprint.pprint(d_yml)
{'file_name': {'logic': 'rule_1 and rule_2',
               'rule_1': {'args': ['s_'],
                          'method': 'startswith',
                          'module': 'str'},
               'rule_2': {'args': ['.log'],
                          'method': 'endswith',
                          'module': 'str'}}}

The below functions help me parse the above rules, and eventually I get:

  • a dictionary of compiled rules
  • the logic to apply the rules to a string (i.e. file name)
def rule_compiler(module, method, args=None, kwargs=None):

    def str_parser(string):
        return getattr(string, method)(*args)
    
    return str_parser
def rule_parser(container):
  
  compiled_rules = {
      k:rule_compiler(**v) for k,v in container.items() if k.startswith('rule')
  }
  
  logic = container.get('logic', None)
  
  return compiled_rules, logic

NOTE: I'm not using module and kwargs here, but they are used elsewhere in the configuration file, for other rules.

So, if I call rule_parser() on the rules' container I get this:

>>> rp = rule_parser(d_yml['file_name'])
>>> print(rp)
({'rule_1': <function __main__.rule_compiler.<locals>.str_parser(string)>,
  'rule_2': <function __main__.rule_compiler.<locals>.str_parser(string)>},
 'rule_1 and rule_2')

If I try to parse a string with every single rule, they work as expected:

>>> fname = 's_test.out'
>>> rp[0]['rule_1'](fname)
True
>>> rp[0]['rule_2'](fname)
False

I want to apply the logic defined by the "logic" and get this:

>>> rp[0]['rule_1'](fname) and rp[0]['rule_2'](fname)
False

I've already thought of a way of doing this by parsing the string containing the boolean logic, transform it into something simimlar to the above, and then call eval() on it.

Can you think of any other way not involving eval()?

NOTE: there might be more than two rules, so replacing the logic with a simple "and/or" and then using some kind of if statement to determine which one to apply wouldn't work either.

NOTE: using regular expressions and remove the need for multiple rules altogether is not an option in this specific case.

Thanks!


Solution

  • Here's an idea, using pandas.eval, not sure how much it suits your case.

    import pandas as pd
    
    fname = 's_test.out'
    rule, logic = rp
    
    def apply_rule(rule_dict: dict, fname: str) -> dict:
        return {rule: func(fname) 
            for rule, func in rule_dict.items()}
    
    print(pd.eval(logic, local_dict = apply_rule(rule, fname)))
    

    Output:

    False