[Zope-Checkins] CVS: Zope2 - Eval.py:1.1.2.2 RestrictionMutator.py:1.1.2.6

evan@serenade.digicool.com evan@serenade.digicool.com
Tue, 24 Apr 2001 20:56:31 -0400


Update of /cvs-repository/Zope2/lib/python/RestrictedPython
In directory serenade.digicool.com:/home/evan/Zope/rp/lib/python/RestrictedPython

Modified Files:
      Tag: RestrictedPythonBranch
	Eval.py RestrictionMutator.py 
Log Message:
Split up compile_restricted, added error handling.
Only load guards if they are used.
Handle printing and guards at module-level.



--- Updated File Eval.py in package Zope2 --
--- Eval.py	2001/04/19 22:50:59	1.1.2.1
+++ Eval.py	2001/04/25 00:56:31	1.1.2.2
@@ -93,24 +93,6 @@
 
 nltosp = string.maketrans('\r\n','  ')
 
-##def careful_mul(env, *factors):
-##    # r = result (product of all factors)
-##    # c = count (product of all non-sequence factors)
-##    # s flags whether any of the factors is a sequence
-##    r=c=1
-##    s=None
-##    for factor in factors:
-##        try:
-##            l=len(factor)
-##            s=1
-##        except TypeError:
-##            c=c*factor
-##        if s and c > 1000:
-##            raise TypeError, \
-##                  'Illegal sequence repeat (too many repetitions: %d)' % c
-##        r=r*factor
-##    return r
-
 default_globals={
     '__builtins__': {},
     '_guard': (lambda ob: ob),
@@ -134,15 +116,14 @@
         if compile_restricted is None:
             # Late binding because this will import the whole
             # compiler suite.
-            from RestrictionMutator import compile_restricted
+            from RestrictionMutator import compile_restricted_eval
 
         expr = strip(expr)
         self.__name__ = expr
         expr = translate(expr, nltosp)
         self.expr = expr
         self.globals = globals
-        used = {}
-        self.code = compile_restricted(expr, '<string>', 'eval', used)
+        self.code, err, warn, used = compile_restricted_eval(expr, '<string>')
         self.used = tuple(used.keys())
 
     def eval(self, mapping):

--- Updated File RestrictionMutator.py in package Zope2 --
--- RestrictionMutator.py	2001/04/24 22:46:06	1.1.2.5
+++ RestrictionMutator.py	2001/04/25 00:56:31	1.1.2.6
@@ -1,11 +1,12 @@
 
-import string
+import sys
 import MutatingWalker
 from compiler import ast, visitor, pycodegen
 from compiler.transformer import Transformer, parse
 from compiler.consts import OP_ASSIGN, OP_DELETE, OP_APPLY
+from parser import ParserError
+from traceback import format_exception_only
 
-
 def rmLineno(node):
     '''Strip lineno attributes from a code tree'''
     if node.__dict__.has_key('lineno'):
@@ -23,14 +24,18 @@
 def exprNode(txt):
     '''Make a "clean" expression node'''
     return stmtNode(txt).expr
+
+_prep_code = {}
+for _n in ('read', 'write'):
+    _prep_code[_n] = [ast.Global(['_%s_guard_' % _n]),
+                      stmtNode('_%s_guard = _%s_guard_' % (_n, _n))]
+_prep_code['print'] = [ast.Global(['_print_target_class']),
+                       stmtNode('_print_target = _print_target_class()')]
 
-_decl_globals_code = ast.Global(['_print_target_class', '_guard_init'])
-_prep_guards_code = stmtNode('_guard = _guard_init')
-_print_code = stmtNode('_print_target = _print_target_class()')
 _printed_expr = exprNode('_print_target()')
 _print_target_name = ast.Name('_print_target')
-_read_guard_name = ast.Name('_guard')
-_write_guard_name = ast.Name('_guard')
+_read_guard_name = ast.Name('_read_guard')
+_write_guard_name = ast.Name('_write_guard')
 
 
 class PrintCollector:
@@ -40,12 +45,14 @@
     def write(self, text):
         self.txt.append(text)
     def __call__(self):
-        return string.join(self.txt, '')
+        return ''.join(self.txt)
 
 
 class FuncInfo:
     _print_used = 0
     _printed_used = 0
+    _read_used = 0
+    _write_used = 0
 
 
 class RestrictionMutator:
@@ -53,23 +60,23 @@
 
     def __init__(self):
         self.warnings = []
+        self.errors = []
         self.used_names = {}
 
-    def raiseSyntaxError(self, node, info):
+    def error(self, node, info):
         lineno = getattr(node, 'lineno', None)
         if lineno is not None and lineno > 0:
-            raise SyntaxError, ('Line %d: %s' % (lineno, info))
+            self.errors.append('Line %d: %s' % (lineno, info))
         else:
-            raise SyntaxError, info
+            self.errors.append(info)
 
     def checkName(self, node, name):
         if len(name) > 1 and name[0] == '_':
             # Note: "_" *is* allowed.
-            self.raiseSyntaxError(
-                node, 'Names starting with "_" are not allowed.')
+            self.error(node, '"%s" is an invalid variable name, because'
+                       ' it starts with "_"' % name)
         if name == 'printed':
-            self.raiseSyntaxError(
-                node, '"printed" is a reserved name.')
+            self.error(node, '"printed" is a reserved name.')
 
     def checkAttrName(self, node):
         # This prevents access to protected attributes of guards
@@ -78,26 +85,34 @@
         name = node.attrname
         if len(name) > 1 and name[0] == '_':
             # Note: "_" *is* allowed.
-            self.raiseSyntaxError(
-                node, 'Attribute names starting with "_" are not allowed.')
+            self.error(node, '"%s" is an invalid attribute name, '
+                       'because it starts with "_".' % name)
+
+    def prepBody(self, body):
+        info = self.funcinfo
+        if info._print_used or info._printed_used:
+            # Add code at top for creating _print_target
+            body[0:0] = _prep_code['print']
+            if not info._printed_used:
+                self.warnings.append(
+                    "Prints, but never reads 'printed' variable.")
+            elif not info._print_used:
+                self.warnings.append(
+                    "Doesn't print, but reads 'printed' variable.")
+        if info._read_used:
+            body[0:0] = _prep_code['read']
+        if info._write_used:
+            body[0:0] = _prep_code['write']
 
     def visitFunction(self, node, walker):
         self.checkName(node, node.name)
         for argname in node.argnames:
             self.checkName(node, argname)
+
         former_funcinfo = self.funcinfo
-        self.funcinfo = funcinfo = FuncInfo()
+        self.funcinfo = FuncInfo()
         node = walker.defaultVisitNode(node)
-        if funcinfo._print_used or funcinfo._printed_used:
-            # Add code at top of function for creating _print_target
-            node.code.nodes.insert(0, _print_code)
-            if not funcinfo._printed_used:
-                self.warnings.append(
-                    "Prints, but never reads 'printed' variable.")
-            elif not funcinfo._print_used:
-                self.warnings.append(
-                    "Doesn't print, but reads 'printed' variable.")
-        node.code.nodes[0:0] = [_decl_globals_code, _prep_guards_code]
+        self.prepBody(node.code.nodes)
         self.funcinfo = former_funcinfo
         return node
 
@@ -108,17 +123,15 @@
 
     def visitPrint(self, node, walker):
         node = walker.defaultVisitNode(node)
-        funcinfo = self.funcinfo
-        if funcinfo is not None:
-            if node.dest is None:
-                funcinfo._print_used = 1
-                node.dest = _print_target_name
+        if node.dest is None:
+            self.funcinfo._print_used = 1
+            node.dest = _print_target_name
         return node
 
     visitPrintnl = visitPrint
 
     def visitName(self, node, walker):
-        if node.name == 'printed' and self.funcinfo is not None:
+        if node.name == 'printed':
             # Replace name lookup with an expression.
             self.funcinfo._printed_used = 1
             return _printed_expr
@@ -134,6 +147,7 @@
         self.checkAttrName(node)
         node = walker.defaultVisitNode(node)
         node.expr = ast.CallFunc(_read_guard_name, [node.expr])
+        self.funcinfo._read_used = 1
         return node
 
     def visitSubscript(self, node, walker):
@@ -141,9 +155,11 @@
         if node.flags == OP_APPLY:
             # get subscript or slice
             node.expr = ast.CallFunc(_read_guard_name, [node.expr])
+            self.funcinfo._read_used = 1
         elif node.flags in (OP_DELETE, OP_ASSIGN):
             # set or remove subscript or slice
             node.expr = ast.CallFunc(_write_guard_name, [node.expr])
+            self.funcinfo._write_used = 1
         return node
 
     visitSlice = visitSubscript
@@ -152,65 +168,127 @@
         self.checkAttrName(node)
         node = walker.defaultVisitNode(node)
         node.expr = ast.CallFunc(_write_guard_name, [node.expr])
+        self.funcinfo._write_used = 1
         return node
 
     def visitExec(self, node, walker):
-        self.raiseSyntaxError(node, 'Exec statements are not allowed.')
+        self.error(node, 'Exec statements are not allowed.')
 
     def visitClass(self, node, walker):
         # Should classes be allowed at all??
         self.checkName(node, node.name)
         return walker.defaultVisitNode(node)
 
+    def visitModule(self, node, walker):
+        self.funcinfo = FuncInfo()
+        node = walker.defaultVisitNode(node)
+        self.prepBody(node.node.nodes)
+        return node
 
-DEBUG = 0
+def getSyntaxError(source, mode):
+    try:
+        compile(source, '<string>', mode)
+    except SyntaxError:
+        err = format_exception_only(SyntaxError, sys.exc_info()[1])
+    else:
+        err = ['Unknown parser error.']
+    return None, err, [], {}
 
-def compile_restricted(s, name, kind,
-                       used_names=None, warnings=None):
-    '''
-    Returns restricted compiled code.
+def tryParsing(source, mode):
+    if mode == 'eval':
+        parser = Transformer().parseexpr
+    else:
+        parser = Transformer().parsesuite
+    try:
+        return parser(source), None
+    except ParserError:
+        return None, getSyntaxError(source, mode)
+
+def compile_restricted_function(p, body, name, filename):
+    '''Compile a restricted code object for a function.
+
+    The function can be reconstituted using the 'new' module:
+
+    new.function(<code>, <globals>)
     '''
+    rm = RestrictionMutator()
+    # Parse the parameters and body, then combine them.
+    tree, err = tryParsing('def f(%s): pass' % p, 'exec')
+    if err:
+        if len(err) > 1:
+            # Drop the first line of the error and adjust the next two.
+            err[1].pop(0) 
+            err[1][0] = 'parameters: %s\n' % err[1][0][10:-8]
+            err[1][1] = '  ' + err[1][1]
+        return err
+    f = tree.node.nodes[0]
+    btree, err = tryParsing(body, 'exec')
+    if err: return err
+    f.code.nodes = btree.node.nodes
+    f.name = name
+    # Look for a docstring
+    stmt1 = f.code.nodes[0]
+    if (isinstance(stmt1, ast.Discard) and
+        isinstance(stmt1.expr, ast.Const) and
+        type(stmt1.expr.value) is type('')):
+        f.doc = stmt1.expr.value
+    MutatingWalker.walk(tree, rm)
+    if rm.errors:
+        return None, rm.errors, rm.warnings, rm.used_names
+    gen = pycodegen.NestedScopeModuleCodeGenerator(filename)
+    visitor.walk(tree, gen)
+    code = gen.getCode().co_consts[1]
+    return code, None, rm.warnings, rm.used_names
+
+def compile_restricted_exec(s, filename='<string>'):
+    '''Compile a restricted code suite.'''
+    rm = RestrictionMutator()
+    tree, err = tryParsing(s, 'exec')
+    if err: return err
+    MutatingWalker.walk(tree, rm)
+    if rm.errors:
+        return None, rm.errors, rm.warnings, rm.used_names
+    gen = pycodegen.NestedScopeModuleCodeGenerator(filename)
+    visitor.walk(tree, gen)
+    return gen.getCode(), None, rm.warnings, rm.used_names
+
+def compile_restricted_eval(s, filename='<string>'):
+    '''Compile a restricted expression.'''
+    rm = RestrictionMutator()
+    tree, err = tryParsing(s, 'eval')
+    if err:
+        err[1].pop(0) # Discard first line of error
+        return err
+    MutatingWalker.walk(tree, rm)
+    if rm.errors:
+        return None, rm.errors, rm.warnings, rm.used_names
+    # XXX No "EvalCodeGenerator" exists
+    # so here's a hack that gets around it.
+    gen = pycodegen.ModuleCodeGenerator(filename)
+    gen.emit('SET_LINENO', 0)
+    visitor.walk(tree, gen)
+    gen.emit('RETURN_VALUE')
+    return gen.getCode(), None, rm.warnings, rm.used_names
+
+DEBUG = 1
+def compile_restricted(source, filename, mode):
+    '''Returns restricted compiled code.'''
     if DEBUG:
         from time import clock
         start = clock()
 
-    rm = RestrictionMutator()
-    if kind == 'exec':
-        tree = Transformer().parsesuite(s)
-        MutatingWalker.walk(tree, rm)
-        gen = pycodegen.NestedScopeModuleCodeGenerator(name)
-        visitor.walk(tree, gen)
-        code = gen.getCode()
-    elif kind == 'eval':
-        tree = Transformer().parseexpr(s)
-        MutatingWalker.walk(tree, rm)
-        # XXX No "EvalCodeGenerator" exists
-        # so here's a hack that gets around it.
-        gen = pycodegen.ModuleCodeGenerator(name)
-        gen.emit('SET_LINENO', 0)
-        visitor.walk(tree, gen)
-        gen.emit('RETURN_VALUE')
-        code = gen.getCode()
+    if mode == 'eval':
+        r = compile_restricted_eval(source, filename)
+    elif mode == 'exec':
+        r = compile_restricted_exec(source, filename)
     else:
-        raise ValueError, 'Unsupported compile kind: ' + kind
+        raise ValueError, "compile_restricted() arg 3 must be 'exec' or 'eval'"
 
-    if used_names is not None:
-        # Fill with the names used.
-        used_names.update(rm.used_names)
-    if warnings is not None:
-        # Fill in warnings.
-        warnings.extend(rm.warnings)
-
     if DEBUG:
         end = clock()
         print 'compile_restricted: %d ms for %s' % (
             (end - start) * 1000, repr(s))
-##        import dis
-##        dis.dis(code)
-##        print `code.co_code`
-    return code
-
-
+    return r
 
 class Noisy:
     '''Test guard class that babbles about accesses'''
@@ -270,8 +348,8 @@
     f = dict['f']
     f.func_globals.update({
         '_print_target_class': PrintCollector,
-        '_guard_init': Noisy,
-        '_guard': Noisy,
+        '_read_guard_': Noisy,
+        '_write_guard_': Noisy,
         })
     print f()
     #import dis