namedutils.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. # -*- coding: utf-8 -*-
  2. # Copyright (c) 2013, Mahmoud Hashemi
  3. #
  4. # Redistribution and use in source and binary forms, with or without
  5. # modification, are permitted provided that the following conditions are
  6. # met:
  7. #
  8. # * Redistributions of source code must retain the above copyright
  9. # notice, this list of conditions and the following disclaimer.
  10. #
  11. # * Redistributions in binary form must reproduce the above
  12. # copyright notice, this list of conditions and the following
  13. # disclaimer in the documentation and/or other materials provided
  14. # with the distribution.
  15. #
  16. # * The names of the contributors may not be used to endorse or
  17. # promote products derived from this software without specific
  18. # prior written permission.
  19. #
  20. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21. # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22. # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23. # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24. # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25. # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26. # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27. # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28. # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31. """\
  32. The ``namedutils`` module defines two lightweight container types:
  33. :class:`namedtuple` and :class:`namedlist`. Both are subtypes of built-in
  34. sequence types, which are very fast and efficient. They simply add
  35. named attribute accessors for specific indexes within themselves.
  36. The :class:`namedtuple` is identical to the built-in
  37. :class:`collections.namedtuple`, with a couple of enhancements,
  38. including a ``__repr__`` more suitable to inheritance.
  39. The :class:`namedlist` is the mutable counterpart to the
  40. :class:`namedtuple`, and is much faster and lighter-weight than
  41. full-blown :class:`object`. Consider this if you're implementing nodes
  42. in a tree, graph, or other mutable data structure. If you want an even
  43. skinnier approach, you'll probably have to look to C.
  44. """
  45. from __future__ import print_function
  46. import sys as _sys
  47. try:
  48. from collections import OrderedDict
  49. except ImportError:
  50. # backwards compatibility (2.6 has no OrderedDict)
  51. OrderedDict = dict
  52. from keyword import iskeyword as _iskeyword
  53. from operator import itemgetter as _itemgetter
  54. try:
  55. basestring
  56. def exec_(code, global_env):
  57. exec("exec code in global_env")
  58. except NameError:
  59. basestring = (str, bytes) # Python 3 compat
  60. def exec_(code, global_env):
  61. exec(code, global_env)
  62. __all__ = ['namedlist', 'namedtuple']
  63. # Tiny templates
  64. _repr_tmpl = '{name}=%r'
  65. _imm_field_tmpl = '''\
  66. {name} = _property(_itemgetter({index:d}), doc='Alias for field {index:d}')
  67. '''
  68. _m_field_tmpl = '''\
  69. {name} = _property(_itemgetter({index:d}), _itemsetter({index:d}), doc='Alias for field {index:d}')
  70. '''
  71. #################################################################
  72. ### namedtuple
  73. #################################################################
  74. _namedtuple_tmpl = '''\
  75. class {typename}(tuple):
  76. '{typename}({arg_list})'
  77. __slots__ = ()
  78. _fields = {field_names!r}
  79. def __new__(_cls, {arg_list}): # TODO: tweak sig to make more extensible
  80. 'Create new instance of {typename}({arg_list})'
  81. return _tuple.__new__(_cls, ({arg_list}))
  82. @classmethod
  83. def _make(cls, iterable, new=_tuple.__new__, len=len):
  84. 'Make a new {typename} object from a sequence or iterable'
  85. result = new(cls, iterable)
  86. if len(result) != {num_fields:d}:
  87. raise TypeError('Expected {num_fields:d}'
  88. ' arguments, got %d' % len(result))
  89. return result
  90. def __repr__(self):
  91. 'Return a nicely formatted representation string'
  92. tmpl = self.__class__.__name__ + '({repr_fmt})'
  93. return tmpl % self
  94. def _asdict(self):
  95. 'Return a new OrderedDict which maps field names to their values'
  96. return OrderedDict(zip(self._fields, self))
  97. def _replace(_self, **kwds):
  98. 'Return a new {typename} object replacing field(s) with new values'
  99. result = _self._make(map(kwds.pop, {field_names!r}, _self))
  100. if kwds:
  101. raise ValueError('Got unexpected field names: %r' % kwds.keys())
  102. return result
  103. def __getnewargs__(self):
  104. 'Return self as a plain tuple. Used by copy and pickle.'
  105. return tuple(self)
  106. __dict__ = _property(_asdict)
  107. def __getstate__(self):
  108. 'Exclude the OrderedDict from pickling' # wat
  109. pass
  110. {field_defs}
  111. '''
  112. def namedtuple(typename, field_names, verbose=False, rename=False):
  113. """Returns a new subclass of tuple with named fields.
  114. >>> Point = namedtuple('Point', ['x', 'y'])
  115. >>> Point.__doc__ # docstring for the new class
  116. 'Point(x, y)'
  117. >>> p = Point(11, y=22) # instantiate with pos args or keywords
  118. >>> p[0] + p[1] # indexable like a plain tuple
  119. 33
  120. >>> x, y = p # unpack like a regular tuple
  121. >>> x, y
  122. (11, 22)
  123. >>> p.x + p.y # fields also accessible by name
  124. 33
  125. >>> d = p._asdict() # convert to a dictionary
  126. >>> d['x']
  127. 11
  128. >>> Point(**d) # convert from a dictionary
  129. Point(x=11, y=22)
  130. >>> p._replace(x=100) # _replace() is like str.replace() but targets named fields
  131. Point(x=100, y=22)
  132. """
  133. # Validate the field names. At the user's option, either generate an error
  134. # message or automatically replace the field name with a valid name.
  135. if isinstance(field_names, basestring):
  136. field_names = field_names.replace(',', ' ').split()
  137. field_names = [str(x) for x in field_names]
  138. if rename:
  139. seen = set()
  140. for index, name in enumerate(field_names):
  141. if (not all(c.isalnum() or c == '_' for c in name)
  142. or _iskeyword(name)
  143. or not name
  144. or name[0].isdigit()
  145. or name.startswith('_')
  146. or name in seen):
  147. field_names[index] = '_%d' % index
  148. seen.add(name)
  149. for name in [typename] + field_names:
  150. if not all(c.isalnum() or c == '_' for c in name):
  151. raise ValueError('Type names and field names can only contain '
  152. 'alphanumeric characters and underscores: %r'
  153. % name)
  154. if _iskeyword(name):
  155. raise ValueError('Type names and field names cannot be a '
  156. 'keyword: %r' % name)
  157. if name[0].isdigit():
  158. raise ValueError('Type names and field names cannot start with '
  159. 'a number: %r' % name)
  160. seen = set()
  161. for name in field_names:
  162. if name.startswith('_') and not rename:
  163. raise ValueError('Field names cannot start with an underscore: '
  164. '%r' % name)
  165. if name in seen:
  166. raise ValueError('Encountered duplicate field name: %r' % name)
  167. seen.add(name)
  168. # Fill-in the class template
  169. fmt_kw = {'typename': typename}
  170. fmt_kw['field_names'] = tuple(field_names)
  171. fmt_kw['num_fields'] = len(field_names)
  172. fmt_kw['arg_list'] = repr(tuple(field_names)).replace("'", "")[1:-1]
  173. fmt_kw['repr_fmt'] = ', '.join(_repr_tmpl.format(name=name)
  174. for name in field_names)
  175. fmt_kw['field_defs'] = '\n'.join(_imm_field_tmpl.format(index=index, name=name)
  176. for index, name in enumerate(field_names))
  177. class_definition = _namedtuple_tmpl.format(**fmt_kw)
  178. if verbose:
  179. print(class_definition)
  180. # Execute the template string in a temporary namespace and support
  181. # tracing utilities by setting a value for frame.f_globals['__name__']
  182. namespace = dict(_itemgetter=_itemgetter,
  183. __name__='namedtuple_%s' % typename,
  184. OrderedDict=OrderedDict,
  185. _property=property,
  186. _tuple=tuple)
  187. try:
  188. exec_(class_definition, namespace)
  189. except SyntaxError as e:
  190. raise SyntaxError(e.message + ':\n' + class_definition)
  191. result = namespace[typename]
  192. # For pickling to work, the __module__ variable needs to be set to the frame
  193. # where the named tuple is created. Bypass this step in environments where
  194. # sys._getframe is not defined (Jython for example) or sys._getframe is not
  195. # defined for arguments greater than 0 (IronPython).
  196. try:
  197. frame = _sys._getframe(1)
  198. result.__module__ = frame.f_globals.get('__name__', '__main__')
  199. except (AttributeError, ValueError):
  200. pass
  201. return result
  202. #################################################################
  203. ### namedlist
  204. #################################################################
  205. _namedlist_tmpl = '''\
  206. class {typename}(list):
  207. '{typename}({arg_list})'
  208. __slots__ = ()
  209. _fields = {field_names!r}
  210. def __new__(_cls, {arg_list}): # TODO: tweak sig to make more extensible
  211. 'Create new instance of {typename}({arg_list})'
  212. return _list.__new__(_cls, ({arg_list}))
  213. def __init__(self, {arg_list}): # tuple didn't need this but list does
  214. return _list.__init__(self, ({arg_list}))
  215. @classmethod
  216. def _make(cls, iterable, new=_list, len=len):
  217. 'Make a new {typename} object from a sequence or iterable'
  218. # why did this function exist? why not just star the
  219. # iterable like below?
  220. result = cls(*iterable)
  221. if len(result) != {num_fields:d}:
  222. raise TypeError('Expected {num_fields:d} arguments,'
  223. ' got %d' % len(result))
  224. return result
  225. def __repr__(self):
  226. 'Return a nicely formatted representation string'
  227. tmpl = self.__class__.__name__ + '({repr_fmt})'
  228. return tmpl % tuple(self)
  229. def _asdict(self):
  230. 'Return a new OrderedDict which maps field names to their values'
  231. return OrderedDict(zip(self._fields, self))
  232. def _replace(_self, **kwds):
  233. 'Return a new {typename} object replacing field(s) with new values'
  234. result = _self._make(map(kwds.pop, {field_names!r}, _self))
  235. if kwds:
  236. raise ValueError('Got unexpected field names: %r' % kwds.keys())
  237. return result
  238. def __getnewargs__(self):
  239. 'Return self as a plain list. Used by copy and pickle.'
  240. return tuple(self)
  241. __dict__ = _property(_asdict)
  242. def __getstate__(self):
  243. 'Exclude the OrderedDict from pickling' # wat
  244. pass
  245. {field_defs}
  246. '''
  247. def namedlist(typename, field_names, verbose=False, rename=False):
  248. """Returns a new subclass of list with named fields.
  249. >>> Point = namedlist('Point', ['x', 'y'])
  250. >>> Point.__doc__ # docstring for the new class
  251. 'Point(x, y)'
  252. >>> p = Point(11, y=22) # instantiate with pos args or keywords
  253. >>> p[0] + p[1] # indexable like a plain list
  254. 33
  255. >>> x, y = p # unpack like a regular list
  256. >>> x, y
  257. (11, 22)
  258. >>> p.x + p.y # fields also accessible by name
  259. 33
  260. >>> d = p._asdict() # convert to a dictionary
  261. >>> d['x']
  262. 11
  263. >>> Point(**d) # convert from a dictionary
  264. Point(x=11, y=22)
  265. >>> p._replace(x=100) # _replace() is like str.replace() but targets named fields
  266. Point(x=100, y=22)
  267. """
  268. # Validate the field names. At the user's option, either generate an error
  269. # message or automatically replace the field name with a valid name.
  270. if isinstance(field_names, basestring):
  271. field_names = field_names.replace(',', ' ').split()
  272. field_names = [str(x) for x in field_names]
  273. if rename:
  274. seen = set()
  275. for index, name in enumerate(field_names):
  276. if (not all(c.isalnum() or c == '_' for c in name)
  277. or _iskeyword(name)
  278. or not name
  279. or name[0].isdigit()
  280. or name.startswith('_')
  281. or name in seen):
  282. field_names[index] = '_%d' % index
  283. seen.add(name)
  284. for name in [typename] + field_names:
  285. if not all(c.isalnum() or c == '_' for c in name):
  286. raise ValueError('Type names and field names can only contain '
  287. 'alphanumeric characters and underscores: %r'
  288. % name)
  289. if _iskeyword(name):
  290. raise ValueError('Type names and field names cannot be a '
  291. 'keyword: %r' % name)
  292. if name[0].isdigit():
  293. raise ValueError('Type names and field names cannot start with '
  294. 'a number: %r' % name)
  295. seen = set()
  296. for name in field_names:
  297. if name.startswith('_') and not rename:
  298. raise ValueError('Field names cannot start with an underscore: '
  299. '%r' % name)
  300. if name in seen:
  301. raise ValueError('Encountered duplicate field name: %r' % name)
  302. seen.add(name)
  303. # Fill-in the class template
  304. fmt_kw = {'typename': typename}
  305. fmt_kw['field_names'] = tuple(field_names)
  306. fmt_kw['num_fields'] = len(field_names)
  307. fmt_kw['arg_list'] = repr(tuple(field_names)).replace("'", "")[1:-1]
  308. fmt_kw['repr_fmt'] = ', '.join(_repr_tmpl.format(name=name)
  309. for name in field_names)
  310. fmt_kw['field_defs'] = '\n'.join(_m_field_tmpl.format(index=index, name=name)
  311. for index, name in enumerate(field_names))
  312. class_definition = _namedlist_tmpl.format(**fmt_kw)
  313. if verbose:
  314. print(class_definition)
  315. def _itemsetter(key):
  316. def _itemsetter(obj, value):
  317. obj[key] = value
  318. return _itemsetter
  319. # Execute the template string in a temporary namespace and support
  320. # tracing utilities by setting a value for frame.f_globals['__name__']
  321. namespace = dict(_itemgetter=_itemgetter,
  322. _itemsetter=_itemsetter,
  323. __name__='namedlist_%s' % typename,
  324. OrderedDict=OrderedDict,
  325. _property=property,
  326. _list=list)
  327. try:
  328. exec_(class_definition, namespace)
  329. except SyntaxError as e:
  330. raise SyntaxError(e.message + ':\n' + class_definition)
  331. result = namespace[typename]
  332. # For pickling to work, the __module__ variable needs to be set to
  333. # the frame where the named list is created. Bypass this step in
  334. # environments where sys._getframe is not defined (Jython for
  335. # example) or sys._getframe is not defined for arguments greater
  336. # than 0 (IronPython).
  337. try:
  338. frame = _sys._getframe(1)
  339. result.__module__ = frame.f_globals.get('__name__', '__main__')
  340. except (AttributeError, ValueError):
  341. pass
  342. return result