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

Viewed 252

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!

2 Answers

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

The right answer could span anywhere from using a library like boolean.py to parse your strings (less flexible, probably easiest choice other than eval) to reading up on lexers and parsers and implementing one from scratch or with a parser generating software (most flexible, but difficult and time-consuming). In this case I'd probably recommend starting with a library specifically built for evaluating boolean expressions and, if that doesn't work, try something more involved.

Edit: I should note that you could use eval if there are no entry points for users to inject malicious code, you properly sanitize the eval string, etc. - probably not worth your time, but worth considering if the other options don't suit your needs.

Related