The str() in the standard library behaves in a slightly weird way when applied against lists: on each element of the list, the repr() is appended. In contrast, this module provides a deep_str() that deeply applies str() across lists.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | """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("<list>")
results.extend(["<item>%s</item>" % deep_str(x)
for x in obj])
results.append("</list>")
return ''.join(results)
def handle_int(obj, deep_str):
if isinstance(obj, int):
return "<int>%s</int>" % obj
def handle_string(obj, deep_str):
if isinstance(obj, str):
return ("<string>%s</string>" %
xml.sax.saxutils.escape(obj))
def handle_default(obj):
return "<unknown/>"
silly = deepstr.DeepStr(handle_default)
silly.recursive_str = deepstr.make_shallow_recursive_str(
"<recursion-detected/>")
silly.register(handle_list)
silly.register(handle_int)
silly.register(handle_string)
print silly([3, 1, "four", [1, "<five>", 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("<list>")
results.extend(["<item>%s</item>" % deep_str(x)
for x in obj])
results.append("</list>")
return ''.join(results)
def handle_int(obj, deep_str):
if isinstance(obj, int):
return "<int>%s</int>" % obj
def handle_string(obj, deep_str):
if isinstance(obj, str):
return "<string>%s</string>" % xml.sax.saxutils.escape(obj)
def handle_default(obj):
return "<unknown/>"
silly = DeepStr(handle_default)
silly.recursive_str = make_shallow_recursive_str(
"<recursion-detected/>")
silly.register(handle_list)
silly.register(handle_int)
silly.register(handle_string)
print silly([3, 1, "four", [1, "<five>", 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()
|
str() has slightly surprising behavior, so this code tries to provide alternative functionality.
Implementing code to do this is actually a bit subtle, since we have to guard against recursive loops, so most of that hard work is handled deep within DeepStr.deepstr(). The unit tests at the bottom show some of the things we have to handle.
I thought it might also be amusing to make it extensible enough to turn it toward dark ends; the _exercise() code shows that it can even be applied to XMLify data structures, though there are better modules for doing this.