Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to evaluate a variable as a Python f-string

I would like to have a mechanism which evaluates an f-string where the contents to be evaluated are provided inside a variable. For example,

x=7
s='{x+x}'
fstr_eval(s)

For the usage case I have in mind, the string s may arise from user input (where the user is trusted with eval).

While using eval in production is generally very bad practice, there are notable exceptions. For instance, the user may be a Python developer, working on a local machine, who would like to use full Python syntax to develop SQL queries.

Note on duplication: There are similar questions here and here. The first question was asked in the limited context of templates. The second question, although very similar to this one, has been marked as a duplicate. Because the context of this question is significantly different from the first, I decided to ask this third question based on the automatically-generated advice following the second question:

This question has been asked before and already has an answer. If those answers do not fully address your question, please ask a new question.

like image 295
Ben Mares Avatar asked Feb 14 '19 23:02

Ben Mares


2 Answers

Even with a trusted user, using eval should only be a very last resort.

If you are willing to sacrifice flexibility of your syntax for a bit more security and control, then you could use str.format and provide it your whole scope.

This will disallow evaluation of expressions, but single variables will be formated into the output.

Code

x = 3
y = 'foo'

s = input('> ')
print(s.format(**vars()))

Example

> {x} and {y}
3 and foo
like image 132
Olivier Melançon Avatar answered Nov 18 '22 15:11

Olivier Melançon


Here is my attempt at more robust evaluation of f-strings, inspired by kadee's elegant answer to a similar question.

I would however like to avoid some basic pitfalls of the eval approach. For instance, eval(f"f'{template}'") fails whenever the template contains an apostrophe, e.g. the string's evaluation becomes f'the string's evaluation' which evaluates with a syntax error. The first improvement is to use triple-apostrophes:

eval(f"f'''{template}'''")

Now it is (mostly) safe to use apostrophes in the template, as long as they are not triple-apostrophes. (Triple-quotes are however fine.) A notable exception is an apostrophe at the end of the string: whatcha doin' becomes f'''whatcha doin'''' which evaluates with a syntax error at the fourth consecutive apostrophe. The following code avoids this particular issue by stripping apostrophes at the end of the string and putting them back after evaluation.

import builtins

def fstr_eval(_s: str, raw_string=False, eval=builtins.eval):
    r"""str: Evaluate a string as an f-string literal.

    Args:
       _s (str): The string to evaluate.
       raw_string (bool, optional): Evaluate as a raw literal 
           (don't escape \). Defaults to False.
       eval (callable, optional): Evaluation function. Defaults
           to Python's builtin eval.

    Raises:
        ValueError: Triple-apostrophes ''' are forbidden.
    """
    # Prefix all local variables with _ to reduce collisions in case
    # eval is called in the local namespace.
    _TA = "'''" # triple-apostrophes constant, for readability
    if _TA in _s:
        raise ValueError("Triple-apostrophes ''' are forbidden. " + \
                         'Consider using """ instead.')

    # Strip apostrophes from the end of _s and store them in _ra.
    # There are at most two since triple-apostrophes are forbidden.
    if _s.endswith("''"):
        _ra = "''"
        _s = _s[:-2]
    elif _s.endswith("'"):
        _ra = "'"
        _s = _s[:-1]
    else:
        _ra = ""
    # Now the last character of s (if it exists) is guaranteed
    # not to be an apostrophe.

    _prefix = 'rf' if raw_string else 'f'
    return eval(_prefix + _TA + _s + _TA) + _ra

Without specifying an evaluation function, this function's local variables are accessible, so

print(fstr_eval(r"raw_string: {raw_string}\neval: {eval}\n_s: {_s}"))

prints

raw_string: False
eval: <built-in function eval>
_s: raw_string: {raw_string}\neval: {eval}\n_s: {_s}

While the prefix _ reduces the likelihood of unintentional collisions, the issue can be avoided by passing an appropriate evaluation function. For instance, one could pass the current global namespace by means of lambda:

fstr_eval('{_s}', eval=lambda expr: eval(expr))#NameError: name '_s' is not defined

or more generally by passing suitable globals and locals arguments to eval, for instance

fstr_eval('{x+x}', eval=lambda expr: eval(expr, {}, {'x': 7})) # 14

I have also included a mechanism to select whether or not \ should be treated as an escape character via the "raw string literal" mechanism. For example,

print(fstr_eval(r'x\ny'))

yields

x
y

while

print(fstr_eval(r'x\ny', raw_string=True))

yields

x\ny

There are likely other pitfalls which I have not noticed, but for many purposes I think this will suffice.

like image 42
Ben Mares Avatar answered Nov 18 '22 17:11

Ben Mares