Please note that the CVS and issue trackers have moved to GitHub. These Trac pages are no longer kept up-to-date.

root/seattle/branches/repy_v2/repy/safe.py

Revision 6917, 24.1 KB (checked in by albert, 17 months ago)

Increasing code safety check timeout for RepyV2 too, see r3410 / #744

Line 
1"""
2  Authors: Phil Hassey, Armon Dadgar, Moshe Kaplan
3
4  Start Date: March 2007
5
6  Description:
7
8  There are 3 main components to this code:
9    Code safety analysis
10      This is done by creating an AST for the code, walking
11      through it node by node, and checking that only safe nodes
12      are used and that no unsafe strings are present.
13
14    Executing safe code
15      This is done by creating a dictionary with a key for each built-in function,
16      and then running the code using that dictionary as our 'context'.
17     
18    SafeDict Class
19      This is a dict that prevents 'unsafe' values from being added.
20      SafeDict is used by virtual_namespace (for the safe eval) as the dictionary
21      of variables that will be accessible to the running code. The reason it is
22      important to prevent unsafe keys is because it is possible to use them to
23      break out of the sandbox. For example, it is possible to change an objects
24      private variables by manually bypassing python's name mangling.
25
26  The original version of this file was written by Phil Hassey. it has since
27  been heavily rewritten for use in the Seattle project.
28
29  Comments:
30
31  Licensing:
32    This file is public domain.
33
34  Authors Comments:
35    Known limitations:
36    - Safe doesn't have any testing for timeouts/DoS.  One-liners
37        like these will lock up the system: "while 1: pass", "234234**234234"
38        This is handled by a seperate portion of Repy which manages the CPU
39        usage.
40    - Lots of (likely) safe builtins and safe AST Nodes are not allowed.
41        I suppose you can add them to the whitelist if you want them.  I
42        trimmed it down as much as I thought I could get away with and still
43        have useful python code.
44    - Might not work with future versions of python - this is made with
45        python 2.4 in mind.  _STR_NOT_BEGIN might have to be extended
46        in the future with more magic variable prefixes.  Or you can
47        switch to conservative mode, but then even variables like "my_var"
48        won't work, which is sort of a nuisance.
49    - If you get data back from a safe_exec, don't call any functions
50        or methods - they might not be safe with __builtin__ restored
51        to its normal state.  Work with them again via an additional safe_exec.
52    - The "context" sent to the functions is not tested at all.  If you
53        pass in a dangerous function {'myfile':file} the code will be able
54        to call it.
55
56"""
57
58# Reference materials:
59
60# Built-in Objects
61# http://docs.python.org/lib/builtin.html
62
63# AST Nodes - compiler
64# http://docs.python.org/lib/module-compiler.ast.html
65
66# Types and members - inspection
67# http://docs.python.org/lib/inspect-types.html
68# The standard type heirarchy
69# http://docs.python.org/ref/types.html
70
71# Based loosely on - Restricted "safe" eval - by Babar K. Zafar
72# (it isn't very safe, but it got me started)
73# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496746
74
75# Securing Python: Controlling the abilities of the interpreter
76# (or - why even trying this is likely to end in tears)
77# http://us.pycon.org/common/talkdata/PyCon2007/062/PyCon_2007.pdf
78
79import os           # This is for some path manipulation
80import sys          # This is to get sys.executable to launch the external process
81import time         # This is to sleep
82
83# Currently required to filter out Android-specific debug messages, cf #1080
84# and safe_check() below
85try:
86  import android
87  IS_ANDROID = True
88except ImportError:
89  IS_ANDROID = False
90
91# Hide the DeprecationWarning for compiler
92import warnings
93warnings.simplefilter('ignore')
94import compiler     # Required for the code safety check
95warnings.resetwarnings()
96
97import UserDict     # This is to get DictMixin
98import platform     # This is for detecting Nokia tablets
99import threading    # This is to get a lock
100import harshexit    # This is to kill the external process on timeout
101import subprocess   # This is to start the external process
102import __builtin__
103import nonportable  # This is to get the current runtime
104import repy_constants # This is to get our start-up directory
105import exception_hierarchy # This is for exception classes shared with tracebackrepy
106
107# Fix to make repy compatible with Python 2.7.2 on Ubuntu 11.10 (ticket #1049)
108subprocess.getattr = getattr
109
110# Armon: This is how long we will wait for the external process
111# to validate the safety of the user code before we timeout,
112# and exit with an exception
113# AR: Increasing timeout to 15 seconds, see r3410 / #744
114EVALUTATION_TIMEOUT = 15
115
116if platform.machine().startswith('armv'):
117  # The Nokia needs more time to evaluate code safety, especially
118  # when under heavy loads
119  EVALUTATION_TIMEOUT = 200
120
121"""
122Repyv2 Changes
123
124NODE_ATTR_OK:
125  Allow '__' in strings.
126    Added: 'value'
127
128
129_NODE_CLASS_OK:
130  Allow exceptions
131    Added: 'TryExcept', 'TryFinally', 'Raise', 'ExcepthandlerType', 'Invert',
132   
133_BUILTIN_OK:   
134  Disallow exiting directly, use exitall instead.
135    Removed: 'exit', 'quit
136 
137  Needed for tracebackrepy
138    Added: 'isinstance', 'BaseException', 'WindowsError', 'type', 'issubclass'
139   
140  Allow primitive marshalling to be built
141    Added: 'ord', 'chr'
142   
143  Repy V2 doesn't allow print()
144    Removed: 'Print', 'Printnl'
145
146_STR_OK:
147  Added:
148    '__repr__', '__str__'
149   
150"""
151
152# This portion of the code is for the Code safety analysis
153# This is done by creating an AST for the code, walking
154# through it node by node, and checking that only safe nodes
155# are used and that no unsafe strings are present.
156
157_STR_OK = ['__init__','__del__','__iter__', '__repr__', '__str__']
158
159# __ is not allowed because it can be used to access a 'private' object in a class
160# by bypassing Python's name mangling.
161_STR_NOT_CONTAIN = ['__']
162_STR_NOT_BEGIN = ['im_','func_','tb_','f_','co_',]
163
164# Disallow these exact strings.
165#   encode and decode are not allowed because of the potential for encoding bugs (#982)
166_STR_NOT_ALLOWED = ['encode','decode'] 
167
168def _is_string_safe(token):
169  """
170  <Purpose>
171    Checks if a string is safe based on rules defined in
172    _STR_OK, _STR_NOT_CONTAIN, and _STR_NOT_BEGIN
173   
174
175  <Arguments>
176    token: A value to check.
177
178  <Returns>
179    True if token is safe, false otherwise
180  """
181
182  # If it's not a string, return True
183  if type(token) is not str and type(token) is not unicode:
184    return True
185 
186  # If the string is explicitly allowed, return True
187  if token in _STR_OK:
188    return True
189   
190  # Check if the string is specifically prohibited:
191  if token in _STR_NOT_ALLOWED:
192    return False
193
194  # Check all the prohibited sub-strings
195  for forbidden_substring in _STR_NOT_CONTAIN:
196    if forbidden_substring in token:
197      return False
198
199  # Check all the prohibited prefixes
200  # Return True if it is safe.
201  return not token.startswith(tuple(_STR_NOT_BEGIN))
202
203
204_NODE_CLASS_OK = [
205    'Add', 'And', 'AssAttr', 'AssList', 'AssName', 'AssTuple',
206    'Assert', 'Assign','AugAssign', 'Bitand', 'Bitor', 'Bitxor', 'Break',
207    'CallFunc', 'Class', 'Compare', 'Const', 'Continue',
208    'Dict', 'Discard', 'Div', 'Ellipsis', 'Expression', 'FloorDiv',
209    'For', 'Function', 'Getattr', 'If', 'Keyword',
210    'LeftShift', 'List', 'ListComp', 'ListCompFor', 'ListCompIf', 'Mod',
211    'Module', 'Mul', 'Name', 'Node', 'Not', 'Or', 'Pass', 'Power',
212    'Return', 'RightShift', 'Slice', 'Sliceobj',
213    'Stmt', 'Sub', 'Subscript', 'Tuple', 'UnaryAdd', 'UnarySub', 'While',
214    # New additions
215    'TryExcept', 'TryFinally', 'Raise', 'ExcepthandlerType', 'Invert',
216    ]
217
218_NODE_ATTR_OK = ['value']
219
220
221def _check_node(node):
222  """
223  <Purpose>
224    Examines a node, its attributes, and all of its children (recursively) for safety.
225    A node is safe if it is in _NODE_CLASS_OK and an attribute is safe if it is
226    not a unicode string and either in _NODE_ATTR_OK or is safe as is
227    defined by _is_string_safe()
228 
229  <Arguments>
230    node: A node in an AST
231   
232  <Exceptions>
233    CheckNodeException if an unsafe node is used
234    CheckStrException if an attribute has an unsafe string
235 
236  <Return>
237    None
238  """
239  if node.__class__.__name__ not in _NODE_CLASS_OK:
240    raise exception_hierarchy.CheckNodeException(node.lineno,node.__class__.__name__)
241 
242  for attribute, value in node.__dict__.iteritems():
243    # Don't allow the construction of unicode literals
244    if type(value) == unicode:
245      raise exception_hierarchy.CheckStrException(node.lineno, attribute, value)
246
247    if attribute in _NODE_ATTR_OK: 
248      continue
249
250    # JAC: don't check doc strings for __ and the like... (#889)
251    if attribute == 'doc' and (node.__class__.__name__ in
252      ['Module', 'Function', 'Class']):
253      continue
254
255
256    # Check the safety of any strings
257    if not _is_string_safe(value):
258      raise exception_hierarchy.CheckStrException(node.lineno, attribute, value)
259
260  for child in node.getChildNodes():
261    _check_node(child)
262
263
264
265def safe_check(code):
266  """
267  <Purpose>
268    Takes the code as input, and parses it into an AST.
269    It then calls _check_node, which does a recursive safety check for every node.
270 
271  <Arguments>
272    code: A string representation of python code
273   
274  <Exceptions>
275    CheckNodeException if an unsafe node is used
276    CheckStrException if an attribute has an unsafe string
277 
278  <Return>
279    None
280  """
281  parsed_ast = compiler.parse(code)
282  _check_node(parsed_ast)
283
284
285# End of the code safety checking implementation
286# Start code safety checking wrappers
287
288
289def safe_check_subprocess(code):
290  """
291  <Purpose>
292    Runs safe_check() in a subprocess. This is done because the AST safe_check()
293    creates uses a large amount of RAM. By running safe_check() in a subprocess
294    we can guarantee that the memory will be reclaimed when the process ends.
295 
296  <Arguments>
297    code: See safe_check.
298   
299  <Exceptions>
300    As with safe_check.
301 
302  <Return>
303    See safe_check.
304  """
305 
306  # Get the path to safe_check.py by using the original start directory of python
307  path_to_safe_check = os.path.join(repy_constants.REPY_START_DIR, "safe_check.py")
308 
309  # Start a safety check process, reading from the user code and outputing to a pipe we can read
310  proc = subprocess.Popen([sys.executable, path_to_safe_check],stdin=subprocess.PIPE, stdout=subprocess.PIPE)
311 
312  # Write out the user code, close so the other end gets an EOF
313  proc.stdin.write(code)
314  proc.stdin.close()
315 
316  # Wait for the process to terminate
317  starttime = nonportable.getruntime()
318
319  # Only wait up to EVALUTATION_TIMEOUT seconds before terminating
320  while nonportable.getruntime() - starttime < EVALUTATION_TIMEOUT:
321    # Did the process finish running?
322    if proc.poll() != None:
323      break;
324    time.sleep(0.02)
325  else:
326    # Kill the timed-out process
327    try:
328      harshexit.portablekill(proc.pid)
329    except:
330      pass
331    raise Exception, "Evaluation of code safety exceeded timeout threshold \
332                    ("+str(nonportable.getruntime() - starttime)+" seconds)"
333 
334  # Read the output and close the pipe
335  rawoutput = proc.stdout.read()
336  proc.stdout.close()
337
338
339  # Interim fix for #1080: Get rid of stray debugging output on Android
340  # of the form "dlopen libpython2.6.so" and "dlopen /system/lib/libc.so",
341  # yet preserve all of the other output (including empty lines).
342
343  if IS_ANDROID:
344    output = ""
345    for line in rawoutput.split("\n"):
346      # Preserve empty lines
347      if line == "":
348        output += "\n"
349        continue
350      # Suppress debug messages we know can turn up
351      wordlist = line.split()
352      if wordlist[0]=="dlopen":
353        if wordlist[-1]=="/system/lib/libc.so":
354          continue
355        if wordlist[-1].startswith("libpython") and \
356          wordlist[-1].endswith(".so"):
357          # We expect "libpython" + version number + ".so".
358          # The version number should be a string convertible to float.
359          # If it's not, raise an exception.
360          try:
361            versionstring = (wordlist[-1].replace("libpython", 
362              "")).replace(".so", "")
363            junk = float(versionstring)
364          except TypeError, ValueError:
365            raise Exception("Unexpected debug output '" + line + 
366              "' while evaluating code safety!")
367      else:
368        output += line + "\n"
369
370    # Strip off the last newline character we added
371    output = output[0:-1]
372
373  else: # We are *not* running on Android, proceed with unfiltered output
374    output = rawoutput
375
376
377  # Check the output, None is success, else it is a failure
378  if output == "None":
379    return True
380 
381  # If there is no output, this is a fatal error condition
382  elif output == "":
383    raise Exception, "Fatal error while evaluating code safety!"
384   
385  else:
386    # Raise the error from the output
387    raise exception_hierarchy.SafeException, output
388
389# Get a lock for serial_safe_check
390SAFE_CHECK_LOCK = threading.Lock()
391
392# Wraps safe_check to serialize calls
393def serial_safe_check(code):
394  """
395  <Purpose>
396    Serializes calls to safe_check_subprocess(). This is because safe_check_subprocess()
397    creates a new process which may take many seconds to return. This prevents us from
398    creating many new python processes.
399 
400  <Arguments>
401    code: See safe_check.
402   
403  <Exceptions>
404    As with safe_check.
405 
406  <Return>
407    See safe_check.
408  """
409
410  SAFE_CHECK_LOCK.acquire()
411  try:
412    return safe_check_subprocess(code)
413  finally:
414    SAFE_CHECK_LOCK.release()
415
416#End of static analysis portion
417
418
419# This portion of the code is for the safe exec.
420# The first step is to create a dictionary with a key for each built-in function
421# We then replace all built-in functions with the values in that dictionary.
422# We then run our code using that dictionary as our 'context'
423# When we're done, we restore the original __builtin__ from a backup
424
425
426# safe replacement for the built-in function `type()`
427_type = type
428_compile_type = _type(compile('','','exec'))
429
430def safe_type(*args, **kwargs):
431  if len(args) != 1 or kwargs:
432    raise exception_hierarchy.RunBuiltinException(
433      'type() may only take exactly one non-keyword argument.')
434
435  # Fix for #1189
436#  if _type(args[0]) is _type or _type(args[0]) is _compile_type:
437#    raise exception_hierarchy.RunBuiltinException(
438#      'unsafe type() call.')
439  # JAC: The above would be reasonable, but it is harsh.   The wrapper code for
440  # the encasement library needs to have a way to check the type of things and
441  # these might be inadvertantly be types.   It is hard to know if something
442  # is a type
443  if args[0] == safe_type or args[0] == _type or _type(args[0]) is _type:
444    return safe_type
445
446  if _type(args[0]) is _type or _type(args[0]) is _compile_type:
447    raise exception_hierarchy.RunBuiltinException(
448      'unsafe type() call.')
449
450  return _type(args[0])
451
452# This dict maps built-in functions to their replacement functions
453_BUILTIN_REPLACE = {
454  'type': safe_type
455}
456
457# The list of built-in exceptions can be generated by running the following:
458# r = [v for v in dir(__builtin__) if v[0] != '_' and v[0] == v[0].upper()] ; r.sort() ; print r
459_BUILTIN_OK = [
460  '__debug__',
461   
462  'ArithmeticError', 'AssertionError', 'AttributeError', 'DeprecationWarning',
463  'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False',
464  'FloatingPointError', 'FutureWarning', 'IOError', 'ImportError',
465  'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt',
466  'LookupError', 'MemoryError', 'NameError', 'None', 'NotImplemented',
467  'NotImplementedError', 'OSError', 'OverflowError', 'OverflowWarning',
468  'PendingDeprecationWarning', 'ReferenceError', 'RuntimeError', 'RuntimeWarning',
469  'StandardError', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError',
470  'SystemExit', 'TabError', 'True', 'TypeError', 'UnboundLocalError',
471  'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError',
472  'UnicodeTranslateError', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError',
473   
474  'abs', 'bool', 'cmp', 'complex', 'dict', 'divmod', 'filter', 'float', 
475  'frozenset', 'hex', 'id', 'int', 'len', 'list', 'long', 'map', 'max', 'min',
476  'object', 'oct', 'pow', 'range', 'reduce', 'repr', 'round', 'set', 'slice',
477  'str', 'sum', 'tuple',  'xrange', 'zip','id',
478   
479  #Added for repyv2
480  'isinstance', 'BaseException', 'WindowsError', 'type', 'issubclass',
481  'ord', 'chr'
482  ]
483
484   
485_BUILTIN_STR = ['copyright','credits','license','__name__','__doc__',]
486
487
488def _replace_unsafe_builtin(unsafe_call):
489  # This function will replace any unsafe built-in function
490  def exceptionraiser(*vargs,**kargs):
491    raise exception_hierarchy.RunBuiltinException(unsafe_call)
492  return exceptionraiser
493
494
495# Stores the current list of allowed built-in functions.
496_builtin_globals = None
497
498# Stores a backup copy of all the built-in functions
499_builtin_globals_backup = None
500
501
502# Populates `_builtin_globals` with keys for every built-in function
503# The values will either be the actual function (if safe), a replacement
504# function, or a stub function that raises an exception.
505def _builtin_init():
506  global _builtin_globals, _builtin_globals_backup
507 
508  # If _builtin_init() was already called there's nothing to do
509  if _builtin_globals != None:
510    return
511 
512  # Create a backup of the built-in functions
513  #TODO: Perhaps pull this out of the function -  Is there a reason to do this more then once?
514  _builtin_globals_backup = __builtin__.__dict__.copy()
515  _builtin_globals = {}
516
517  for builtin in __builtin__.__dict__.iterkeys():
518    # It's important to check _BUILTIN_REPLACE before _BUILTIN_OK because
519    # even if the name is defined in both, there must be a security reason
520    # why it was supposed to be replaced, and not just allowed.
521    if builtin in _BUILTIN_REPLACE:
522      replacewith = _BUILTIN_REPLACE[builtin]
523    elif builtin in _BUILTIN_OK: 
524     replacewith = __builtin__.__dict__[builtin]
525    elif builtin in _BUILTIN_STR:
526      replacewith = ''
527    else:
528      # Replace the function with our exception-raising variant
529      replacewith = _replace_unsafe_builtin(builtin)
530    _builtin_globals[builtin] = replacewith
531
532  # Armon: Make SafeDict available
533  _builtin_globals["SafeDict"] = get_SafeDict
534
535  # Make the repy exception hierarchy available
536  # This is done by making every exception in _EXPORTED_EXCEPTIONS
537  # available as a built-in
538  for exception_name in exception_hierarchy._EXPORTED_EXCEPTIONS:
539    _builtin_globals[exception_name] = exception_hierarchy.__dict__[exception_name]
540
541# Replace every function in __builtin__ with the one from _builtin_globals.
542def _builtin_destroy():
543  _builtin_init()
544  for builtin_name, builtin in _builtin_globals.iteritems():
545    __builtin__.__dict__[builtin_name] = builtin
546
547# Restore every function in __builtin__ with the backup from _builtin_globals_backup.
548def _builtin_restore():
549  for builtin_name, builtin in _builtin_globals_backup.iteritems():
550    __builtin__.__dict__[builtin_name] = builtin
551
552# Have the builtins already been destroyed?
553BUILTINS_DESTROYED = False
554
555
556def safe_run(code,context=None):
557  """
558  <Purpose>
559    Executes code with only safe builtins.
560    If context is passed in, those keys will be available to the code.
561 
562  <Arguments>
563    code: A string representation of python code
564    context: A dictionary of variables to execute 'in'
565   
566  <Exceptions>
567    exception_hierarchy.RunBuiltinException if an unsafe call is made
568    Whatever else the source code may raise
569 
570  <Return>
571    None
572  """
573 
574  global BUILTINS_DESTROYED
575 
576  if context == None:
577    context = {}
578 
579  # Destroy the builtins if needed
580  if not BUILTINS_DESTROYED:
581    BUILTINS_DESTROYED = True
582    _builtin_destroy()
583   
584  try:
585    context['__builtins__'] = _builtin_globals
586    exec code in context
587  finally:
588    #_builtin_restore()
589    pass
590
591
592# Convenience functions
593
594def safe_exec(code, context = None):
595  """
596  <Purpose>
597    Checks the code for safety. It then executes code with only safe builtins.
598    This is a wrapper for calling serial_safe_check() and safe_run()
599 
600  <Arguments>
601    code: A string representation of python code
602    context: A dictionary of variables to execute 'in'
603   
604  <Exceptions>
605    CheckNodeException if an unsafe node is used
606    CheckStrException if an attribute has an unsafe string
607    exception_hierarchy.RunBuiltinException if an unsafe call is made
608    Whatever else the code may raise
609 
610  <Return>
611    None
612  """
613
614  serial_safe_check(code)
615  safe_run(code, context)
616
617
618
619# This portion of the code defines a SafeDict
620# A SafeDict prevents keys which are 'unsafe' strings from being added.
621
622
623# Functional constructor for SafeDict to allow us to safely map it into the repy context.
624def get_SafeDict(*args,**kwargs):
625  return SafeDict(*args,**kwargs)
626
627
628class SafeDict(UserDict.DictMixin):
629  """
630  <Purpose>
631    A dictionary implementation which prohibits "unsafe" keys from being set or
632    get. This is done by checking the key with _is_string_safe().
633   
634    SafeDict is used by virtual_namespace (for the safe eval) as the dictionary
635    of variables that will be accessible to the running code. The reason it is
636    important to prevent unsafe keys is because it is possible to use them to
637    break out of the sandbox. For example, it is possible to change an object's
638    private variables by manually bypassing python's name mangling.
639  """
640
641  def __init__(self,from_dict=None):
642    # Create the underlying dictionary
643    self.__under__ = {}
644
645    # Break if we are done...
646    if from_dict is None:
647      return
648    if type(from_dict) is not dict and not isinstance(from_dict,SafeDict):
649      return
650
651    # If we are given a dict, try to copy its keys
652    for key,value in from_dict.iteritems():
653      # Skip __builtins__ and __doc__ since safe_run/python inserts that
654      if key in ["__builtins__","__doc__"]:
655        continue
656
657      # Check the key type
658      if type(key) is not str and type(key) is not unicode:
659        raise TypeError, "'SafeDict' keys must be of string type!"
660
661      # Check if the key is safe
662      if _is_string_safe(key):
663        self.__under__[key] = value
664
665      # Throw an exception if the key is unsafe
666      else:
667        raise ValueError, "Unsafe key: '"+key+"'"
668
669  # Allow getting items
670  def __getitem__(self,key):
671    if type(key) is not str and type(key) is not unicode:
672      raise TypeError, "'SafeDict' keys must be of string type!"
673    if not _is_string_safe(key):
674      raise ValueError, "Unsafe key: '"+key+"'"
675
676    return self.__under__.__getitem__(key)
677
678  # Allow setting items
679  def __setitem__(self,key,value):
680    if type(key) is not str and type(key) is not unicode:
681      raise TypeError, "'SafeDict' keys must be of string type!"
682    if not _is_string_safe(key):
683      raise ValueError, "Unsafe key: '"+key+"'"
684
685    return self.__under__.__setitem__(key,value)
686
687  # Allow deleting items
688  def __delitem__(self,key):
689    if type(key) is not str and type(key) is not unicode:
690      raise TypeError, "'SafeDict' keys must be of string type!"
691    if not _is_string_safe(key):
692      raise ValueError, "Unsafe key: '"+key+"'"
693
694    return self.__under__.__delitem__(key)
695
696  # Allow checking if a key is set
697  def __contains__(self,key):
698    if type(key) is not str and type(key) is not unicode:
699      raise TypeError, "'SafeDict' keys must be of string type!"
700    if not _is_string_safe(key):
701      raise ValueError, "Unsafe key: '"+key+"'"
702
703    return key in self.__under__
704
705  # Return the key set
706  def keys(self):
707
708    # Filter out the unsafe keys from the underlying dict
709    safe_keys = []
710
711    for key in self.__under__.iterkeys():
712      if _is_string_safe(key):
713        safe_keys.append(key)
714
715    # Return the safe keys
716    return safe_keys
717
718  # allow us to be printed
719  # this gets around the __repr__ infinite loop issue ( #918 ) for simple cases
720  # It seems unlikely this is adequate for more complex cases (like safedicts
721  # that refer to each other)
722  def __repr__(self):
723    newdict = {}
724    for safekey in self.keys():
725      if self.__under__[safekey] == self:
726        newdict[safekey] = newdict
727      else:
728        newdict[safekey] = self.__under__[safekey]
729    return newdict.__repr__()
730
731
732  # Allow a copy of us
733  def copy(self):
734    # Create a new instance
735    copy_inst = SafeDict(self.__under__)
736
737    # Return the new instance
738    return copy_inst
739
740  # Make our fields read-only
741  # This means __getattr__ can do its normal thing, but any
742  # setters need to be overridden to prohibit adding/deleting/updating
743
744  def __setattr__(self,name,value):
745    # Allow setting __under__ on initialization
746    if name == "__under__" and name not in self.__dict__:
747      self.__dict__[name] = value
748      return
749    raise TypeError,"'SafeDict' attributes are read-only!"
750
751  def __delattr__(self,name):
752    raise TypeError,"'SafeDict' attributes are read-only!"
Note: See TracBrowser for help on using the browser.