improve recursive access and do some minor eval/exec fixes
authorrobin
Thu, 24 Oct 2019 15:52:32 +0100
changeset 4549 7926f4988eb1
parent 4548 f6ba8d75d3df
child 4550 80dd9e83dad9
improve recursive access and do some minor eval/exec fixes
src/reportlab/lib/utils.py
tests/test_lib_utils.py
--- a/src/reportlab/lib/utils.py	Thu Oct 24 15:50:50 2019 +0100
+++ b/src/reportlab/lib/utils.py	Thu Oct 24 15:52:32 2019 +0100
@@ -4,7 +4,9 @@
 __version__='3.3.0'
 __doc__='''Gazillions of miscellaneous internal utility functions'''
 
-import os, sys, time, types, datetime
+import os, sys, time, types, datetime, ast
+from functools import reduce as functools_reduce
+literal_eval = ast.literal_eval
 from base64 import decodestring as base64_decodestring, encodestring as base64_encodestring
 from reportlab import isPy3
 from reportlab.lib.logger import warnOnce
@@ -240,7 +242,7 @@
             del frame
         elif L is None:
             L = G
-        exec("""exec obj in G, L""")
+        exec(obj,G, L)
     rl_exec("""def rl_reraise(t, v, b=None):\n\traise t, v, b\n""")
 
     char2int = ord
@@ -490,22 +492,6 @@
     finally:
         sys.path = opath
 
-def recursiveGetAttr(obj, name):
-    "Can call down into e.g. object1.object2[4].attr"
-    return eval(name, obj.__dict__)
-
-def recursiveSetAttr(obj, name, value):
-    "Can call down into e.g. object1.object2[4].attr = value"
-    #get the thing above last.
-    tokens = name.split('.')
-    if len(tokens) == 1:
-        setattr(obj, name, value)
-    else:
-        most = '.'.join(tokens[:-1])
-        last = tokens[-1]
-        parent = recursiveGetAttr(obj, most)
-        setattr(parent, last, value)
-
 def import_zlib():
     try:
         import zlib
@@ -558,9 +544,9 @@
             elif isinstance(v,int):
                 v = int(av)
             elif isinstance(v,list):
-                v = list(eval(av))
+                v = list(literal_eval(av),{})
             elif isinstance(v,tuple):
-                v = tuple(eval(av))
+                v = tuple(literal_eval(av),{})
             else:
                 raise TypeError("Can't convert string %r to %s" % (av,type(v)))
         return v
@@ -575,7 +561,7 @@
         handled = 0
         ke = k+'='
         for a in A:
-            if a.find(ke)==0:
+            if a.startswith(ke):
                 av = a[len(ke):]
                 A.remove(a)
                 R[k] = handleValue(v,av,func)
@@ -1516,3 +1502,89 @@
             a[2] = a[2].lstrip('0')
             a = ' '.join(a)
         return a
+
+def safer_globals(g=None):
+    if g is None:
+        g = sys._getframe(1).f_globals.copy()
+    for name in ('__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__'):
+        if name in g:
+            del g[name]
+    return g
+
+###############################################################
+#the following code has been (thanks to MIT license)
+#freely adapted from https://github.com/frmdstryr/magicattr
+#the function names are changed to avoid clasing with the actual
+#magicattr names to prevent confusion should others be using that
+#as well as ReportLab
+from functools import reduce as functools_reduce
+
+#: Types of AST nodes that are used
+_raccess_ast_types = (ast.Name, ast.Attribute, ast.Subscript, ast.Call)
+
+def recursiveGetAttr(ob, a):
+    return functools_reduce(_raccess_getattr, _raccess_get_nodes(a), ob)
+
+def recursiveSetAttr(ob, a, val):
+    ob, attr_or_key, is_subscript = raccess_lookup(ob, a)
+    if is_subscript:
+        ob[attr_or_key] = val
+    else:
+        setattr(ob, attr_or_key, val)
+
+def recursiveDelAttr(ob, a):
+    ob, attr_or_key, is_subscript = raccess_lookup(ob, a)
+    if is_subscript:
+        del ob[attr_or_key]
+    else:
+        delattr(ob, attr_or_key)
+
+def raccess_lookup(ob, a):
+    N = tuple(_raccess_get_nodes(a))
+    if len(N) > 1:
+        ob = functools_reduce(_raccess_getattr, N[:-1], ob)
+        n = N[-1]
+    else:
+        n = N[0]
+    if isinstance(n, ast.Attribute):
+        return ob, n.attr, False
+    elif isinstance(n, ast.Subscript):
+        return ob, _raccess_getitem(n.slice.value), True
+    elif isinstance(n, ast.Name):
+        return ob, n.id, False
+    raise NotImplementedError("access by %s is not supported" % n)
+
+def _raccess_get_nodes(a):
+    if not isStr(a):
+        raise TypeError("Attribute name must be a string not %s" % repr(a))
+    if not isNative(a):
+        a = asNative(a)
+    N = ast.parse(a).body
+    if not N or not isinstance(N[0], ast.Expr):
+        raise ValueError("Invalid expression: %s"%a)
+    return reversed([n for n in ast.walk(N[0])
+                     if isinstance(n, _raccess_ast_types)])
+
+def _raccess_getitem(n):
+    # Handle indexes
+    if isinstance(n, ast.Num):
+        return n.n
+    # Handle string keys
+    elif isinstance(n, ast.Str):
+        return n.s
+    # Handle negative indexes
+    elif (isinstance(n, ast.UnaryOp) and isinstance(n.op, ast.USub)
+          and isinstance(n.operand, ast.Num)):
+        return -n.operand.n
+    raise NotImplementedError("subscripting unsupported for node: %s" % n)
+
+def _raccess_getattr(ob, n):
+    if isinstance(n, ast.Attribute):
+        return getattr(ob, n.attr)
+    elif isinstance(n, ast.Subscript):
+        return ob[_raccess_getitem(n.slice.value)]
+    elif isinstance(n, ast.Name):
+        return getattr(ob, n.id)
+    elif isinstance(n, ast.Call):
+        raise ValueError("Function calls are not allowed.")
+    raise NotImplementedError("unsupported node: %s" % n)
--- a/tests/test_lib_utils.py	Thu Oct 24 15:50:50 2019 +0100
+++ b/tests/test_lib_utils.py	Thu Oct 24 15:52:32 2019 +0100
@@ -11,7 +11,8 @@
 import unittest
 from reportlab.lib import colors
 from reportlab.lib.utils import recursiveImport, recursiveGetAttr, recursiveSetAttr, rl_isfile, \
-                                isCompactDistro, isPy3, isPyPy, TimeStamp, rl_get_module
+                                isCompactDistro, isPy3, isPyPy, TimeStamp, rl_get_module, \
+                                recursiveGetAttr, recursiveSetAttr, recursiveDelAttr
 
 def _rel_open_and_read(fn):
     from reportlab.lib.utils import open_and_read
@@ -207,8 +208,139 @@
         ff.search()
         ff.getFamilyNames()
 
+
+class RaccessTest:
+    l = [1, 2]
+    a = [0, [1, 2, [3,4]]]
+    b = {'x': {'y': 'y'}, 'z': [1, 2]}
+    z = 'z'
+
+class RaccessPerson:
+    settings = {
+        'autosave': True,
+        'style': {
+            'height': 30,
+            'width': 200
+        },
+        'themes': ['light', 'dark']
+    }
+    def __init__(self, name, age, friends):
+        self.name = name
+        self.age = age
+        self.friends = friends
+
+class RaccessTestCase(unittest.TestCase):
+    "Test recursive access functions"
+    def test1(self):
+        def innerTest(k,v):
+            obj = RaccessTest()
+            obj.t = obj
+            obj.a.append(obj)
+            obj.b['w'] = obj
+            self.assertEqual(recursiveGetAttr(obj,k),v,"error getattr(obj,%r)==%r" % (k,v))
+        for k,v in [
+            ('l', RaccessTest.l),
+            ('t.t.t.t.z', 'z'),
+            ('a[0]', 0),
+            ('a[1][0]', 1),
+            ('a[1][2]', [3,4]),
+            ('b["x"]', {'y': 'y'}),
+            ('b["x"]["y"]', 'y'),
+            ('b["z"]', [1,2]),
+            ('b["z"][1]', 2),
+            ('b["w"].z', 'z'),
+            ('b["w"].t.l', [1, 2]),
+            ('a[-1].z', 'z'),
+            ('l[-1]', 2),
+            ('a[2].t.a[-1].z', 'z'),
+            ('a[2].t.b["z"][0]', 1),
+            ('a[-1].t.z', 'z'),
+            ]:
+            innerTest(k,v)
+
+    def test_person_example(self):
+        bob = RaccessPerson(name="Bob", age=31, friends=[])
+        jill = RaccessPerson(name="Jill", age=29, friends=[bob])
+        jack = RaccessPerson(name="Jack", age=28, friends=[bob, jill])
+
+        # Nothing new
+        self.assertEqual(recursiveGetAttr(bob, 'age') ,31)
+
+        # Lists
+        self.assertEqual(recursiveGetAttr(jill, 'friends[0].name') ,'Bob')
+        self.assertEqual(recursiveGetAttr(jack, 'friends[-1].age') ,29)
+
+        # Dict lookups
+        self.assertEqual(recursiveGetAttr(jack, 'settings["style"]["width"]') ,200)
+
+        # Combination of lookups
+        self.assertEqual(recursiveGetAttr(jack, 'settings["themes"][-2]') ,'light')
+        self.assertEqual(recursiveGetAttr(jack, 'friends[-1].settings["themes"][1]') ,'dark')
+
+        # Setattr
+        recursiveSetAttr(bob, 'settings["style"]["width"]', 400)
+        self.assertEqual(recursiveGetAttr(bob, 'settings["style"]["width"]') ,400)
+
+        # Nested objects
+        recursiveSetAttr(bob, 'friends', [jack, jill])
+        self.assertEqual(recursiveGetAttr(jack, 'friends[0].friends[0]') ,jack)
+
+        recursiveSetAttr(jill, 'friends[0].age', 32)
+        self.assertEqual(bob.age ,32)
+
+        # Deletion
+        recursiveDelAttr(jill, 'friends[0]')
+        self.assertEqual(len(jill.friends) ,0)
+
+        recursiveDelAttr(jill, 'age')
+        assert not hasattr(jill, 'age')
+
+        recursiveDelAttr(bob, 'friends[0].age')
+        assert not hasattr(jack, 'age')
+
+        # Unsupported
+        with self.assertRaises(NotImplementedError) as e:
+            recursiveGetAttr(bob, 'friends[0+1]')
+
+        # Nice try, function calls are not allowed
+        with self.assertRaises(ValueError):
+            recursiveGetAttr(bob, 'friends.pop(0)')
+
+        # Must be an expression
+        with self.assertRaises(ValueError):
+            recursiveGetAttr(bob, 'friends = []')
+
+        # Must be an expression
+        with self.assertRaises(SyntaxError):
+            recursiveGetAttr(bob, 'friends..')
+
+        # Must be an expression
+        with self.assertRaises(KeyError):
+            recursiveGetAttr(bob, 'settings["DoesNotExist"]')
+
+        # Must be an expression
+        with self.assertRaises(IndexError):
+            recursiveGetAttr(bob, 'friends[100]')
+
+    def test_empty(self):
+        obj = RaccessTest()
+        with self.assertRaises(ValueError):
+           recursiveGetAttr(obj,"  ")
+
+        with self.assertRaises(ValueError):
+            recursiveGetAttr(obj,"")
+
+        with self.assertRaises(TypeError):
+            recursiveGetAttr(obj, 0)
+
+        with self.assertRaises(TypeError):
+            recursiveGetAttr(obj, None)
+
+        with self.assertRaises(TypeError):
+            recursiveGetAttr(obj, obj)
+
 def makeSuite():
-    return makeSuiteForClasses(ImporterTestCase)
+    return makeSuiteForClasses(ImporterTestCase,RaccessTestCase)
 
 if __name__ == "__main__": #noruntests
     unittest.TextTestRunner().run(makeSuite())