"""Utilities for handling deep str()ingification. Danny Yoo (dyoo@hkn.eecs.berkeley.edu) Casual Usage: from deepstr import deep_str print deep_str(["hello", "world"]) Very unusual usage: import deepstr import xml.sax.saxutils def handle_list(obj, deep_str): if isinstance(obj, list): results = [] results.append("") results.extend(["%s" % deep_str(x) for x in obj]) results.append("") return ''.join(results) def handle_int(obj, deep_str): if isinstance(obj, int): return "%s" % obj def handle_string(obj, deep_str): if isinstance(obj, str): return ("%s" % xml.sax.saxutils.escape(obj)) def handle_default(obj): return "" silly = deepstr.DeepStr(handle_default) silly.recursive_str = deepstr.make_shallow_recursive_str( "") silly.register(handle_list) silly.register(handle_int) silly.register(handle_string) print silly([3, 1, "four", [1, "", 9.0]]) x = [] x.append(x) print silly(x) This module provides a function called deep_str() that will do a deep str() on objects. This module also provides utilities to develop custom str() functions. (What's largely undocumented is the fact that this isn't really about strings, but can be used as a general --- and convoluted --- framework for mapping some process across data.) """ import unittest def make_shallow_recursive_str(recursion_label): def f(obj, deep_str): return recursion_label return f class DeepStr: """Deep stringifier.""" def __init__(self, default_str=str, recursive_str=make_shallow_recursive_str("...")): """ DeepStr: stringify_function handler -> stringify_function Creates a new DeepStr. Once constructed, you can call as if this were a function that takes objects and returns strings. default_str is the default function used on types that this does not recognize. It must be able to take in an object and return a string. If we hit structure that's already been traversed, we use recursive_str on that structure.""" self.handlers = [] self.default_str = default_str self.recursive_str = recursive_str def __call__(self, obj): """Takes an object and returns a string of that object.""" return self.deepstr(obj, {}) def deepstr(self, obj, seen): ## Notes: this code is a little trickier than I'd like, but ## I don't see a good way of simplifying it yet. Subtle parts ## include the construction of substructure_deepstr, and use ## of a fresh dictionary in 'new_seen'. if id(obj) in seen: ## TRICKY CODE: the recursive function is taking in a ## stringifier whose 'seen' dictionary is empty. def fresh_deepstr(sub_obj): return self.deepstr(sub_obj, {}) return self.recursive_str(obj, fresh_deepstr) def substructure_deepstr(sub_obj): new_seen = dict(seen) new_seen[id(obj)] = True return self.deepstr(sub_obj, new_seen) for h in self.handlers: result = h(obj, substructure_deepstr) if result != None: return result return self.default_str(obj) def register(self, handler): """register: (object str_function -> string or None) Registers a new handler type. Handers take in the object as well as a str() function, and returns either a string if it can handle the object, or None otherwise. The second argument should be used on substructures.""" self.handlers.append(handler) ###################################################################### ## Below here is a sample implementation for deep_str() def handle_list(obj, deep_str): if isinstance(obj, list): return "[" + ", ".join([deep_str(x) for x in obj]) + "]" return None def handle_tuple(obj, deep_str): if isinstance(obj, tuple): return "(" + ", ".join([deep_str(x) for x in obj]) + ")" return None def handle_dict(obj, deep_str): if isinstance(obj, dict): return ("{" + ", ".join([deep_str(k) + ': ' + deep_str(v) for (k, v) in obj.items()]) + "}") return None def handle_recursion(obj, deep_str): if isinstance(obj, list): return "[...]" ## tuples aren't handled; from my best understanding, ## it's not possible to construct a tuple that contains itself. if isinstance(obj, dict): return "{...}" return "..." deep_str = DeepStr(str, handle_recursion) deep_str.register(handle_list) deep_str.register(handle_tuple) deep_str.register(handle_dict) ###################################################################### ## Sample exercising code. This is here just to show a wacky example. def _exercise(): import xml.sax.saxutils def handle_list(obj, deep_str): if isinstance(obj, list): results = [] results.append("") results.extend(["%s" % deep_str(x) for x in obj]) results.append("") return ''.join(results) def handle_int(obj, deep_str): if isinstance(obj, int): return "%s" % obj def handle_string(obj, deep_str): if isinstance(obj, str): return "%s" % xml.sax.saxutils.escape(obj) def handle_default(obj): return "" silly = DeepStr(handle_default) silly.recursive_str = make_shallow_recursive_str( "") silly.register(handle_list) silly.register(handle_int) silly.register(handle_string) print silly([3, 1, "four", [1, "", 9.0]]) x = [] x.append(x) print silly(x) ###################################################################### ## Test cases class MyTests(unittest.TestCase): def testSimpleThings(self): for obj in [42, 'hello', 0+1j, 2.3, u'world']: self.assertEquals(str(obj), deep_str(obj)) def testSimpleLists(self): self.assertEquals(str([1, 2, 3]), deep_str([1, 2, 3])) def testListsWithStrings(self): self.assertEquals("[hello, world]", deep_str(["hello", "world"])) def testRepeatedObjects(self): self.assertEquals("[1, 1]", deep_str([1, 1])) def testRecursion(self): L = [1, 2] L.append(L) self.assertEquals("[1, 2, [...]]", deep_str(L)) def testSimpleDict(self): self.assertEquals("{hello: world}", deep_str({'hello' : 'world'})) def testDictWithRecursion(self): D = {} D[1] = D self.assertEquals("{1: {...}}", deep_str(D)) def testNonRecursion(self): a = ['a'] L = [a, a] self.assertEquals("[[a], [a]]", deep_str(L)) if __name__ == '__main__': unittest.main()