How does python interpret variable attributes in strings

138 Views Asked by At

Suppose I have a string like this

"Style.BRIGHT + 'BRIGHT' + '\\n' + Style.DIM + 'DIM' + '\\n' + Style.NORMAL + 'NORMAL'"

I try to print it directly

from colorama import Style

s = "Style.BRIGHT + 'BRIGHT' + '\\n' + Style.DIM + 'DIM' + '\\n' + Style.NORMAL + 'NORMAL'"
print(s)

Output:

Style.BRIGHT + 'BRIGHT' + '\n' + Style.DIM + 'DIM' + '\n' + Style.NORMAL + 'NORMAL'

This is not the result I want, I want it to display the result in the terminal like this.

enter image description here

So I tried eval and it worked.

from colorama import Style

s = "Style.BRIGHT + 'BRIGHT' + '\\n' + Style.DIM + 'DIM' + '\\n' + Style.NORMAL + 'NORMAL'"
print(eval(s))

Output:

BRIGHT
DIM
NORMAL

Although it succeeded, it seems to be unsafe, I also tried adding the global variable {"Style": Style}, so it might be relatively safe.

But maybe I'll extract properties in other variables, like this x.y, where x and y can be any value. So it feels like eval doesn't apply.

The answer of @jfs in this question seems to be what I want, and I modified the program.

from colorama import Style

import ast
import operator as op
# supported operators
operators = {ast.Add: op.add}


def eval_expr(expr):

    return eval_(ast.parse(expr, mode='eval').body)


def eval_(node):
    if isinstance(node, ast.Constant):  # <number>
        return node.n
    elif isinstance(node, ast.BinOp):  # <left> <operator> <right>
        return operators[type(node.op)](eval_(node.left), eval_(node.right))
    elif isinstance(node, ast.Attribute):
        return ast.unparse(node)
    else:
        raise TypeError(node)


s = "Style.BRIGHT + 'BRIGHT' + '\\n' + Style.DIM + 'DIM' + '\\n' + Style.NORMAL + 'NORMAL'"
print(eval_expr(s))

Output:

Style.BRIGHTBRIGHT
Style.DIMDIM
Style.NORMALNORMAL

In the program the string Style.BRIGHT is converted into an ast.Attribute object, I don't know how to get its value('\x1b[1m').

In [1]: from colorama import Style

In [2]: Style.BRIGHT
Out[2]: '\x1b[1m'

If I use eval(ast.unparse(node)) it succeeds, but if so why not just use eval directly...

So my question is:

  • How can I extract the primitive value corresponding to the ast.Attribute object?

Or is there any other way to interpret the string.


The demo above is just one case, it can be applied to other cases. For example, I want to define a macro, which is a string that adds and sums some properties of a class, which will be applied to other programs or processes. Demo:

class Base:

    a = 1
    b = 2


def get(cls):
    s = []
    for k, v in cls.__dict__.items():
        if k.startswith("_"):
            continue
        s.append(f"{cls.__name__}.{k}")
    return " + ".join(s)


s = get(Base)
print(s)

# Base.a + Base.b

Maybe my case is a bit unsatisfactory, but this problem may appear in a future scenario, maybe it's just me wanting to know how to deal with it.

1

There are 1 best solutions below

2
mozway On

NB. It would be hundred times better to use the correct object and not a string in the first place, starting with correct input is almost always more explicit and efficient than trying to fix a broken input.

If you have a string with arbitrarily complex operations, you will need to understand its grammar, which is exactly what ast is doing, or blindly (and potentially unsafely) evaluate it with eval.

That said, if you want to simplify your process in the particular case you showed, you need to do 3 things. 1- replace the Styler definitions with the string formatting codes they represent, 2- remove the quotes and +, 3- unescape the escaped newlines.

2 and 3 are quite easy to achieve, so I'll focus only on interpreting the codes here. You can use a regex to find the Styler definitions and re.sub to replace them safely with the actual formatting code.

from colorama import Style
import re

s = "Style.BRIGHT + 'BRIGHT' + '\\n' + Style.DIM + 'DIM' + '\\n' + Style.NORMAL + 'NORMAL'"

s2 = re.sub('Style\.[A-Z]+', lambda m: getattr(Style, m.group(0).split('.')[1]), s)

Output:

"\x1b[1m + 'BRIGHT' + '\\n' + \x1b[2m + 'DIM' + '\\n' + \x1b[22m + 'NORMAL'"