457 lines
14 KiB
Python
457 lines
14 KiB
Python
"""Those tests are mostly a copy paste of cpython unit tests for namedtuple, with a few differences to reflect the
|
|
implementation details that differs. It ensures that we caught the same edge cases as they did."""
|
|
|
|
import collections
|
|
import copy
|
|
import pickle
|
|
import string
|
|
import sys
|
|
import unittest
|
|
from collections import OrderedDict
|
|
from random import choice
|
|
|
|
from bonobo.util.bags import BagType
|
|
|
|
################################################################################
|
|
### Named Tuples
|
|
################################################################################
|
|
|
|
TBag = BagType('TBag', ('x', 'y', 'z')) # type used for pickle tests
|
|
|
|
|
|
class TestBagType(unittest.TestCase):
|
|
def _create(self, *fields, typename='abc'):
|
|
bt = BagType(typename, fields)
|
|
assert bt._fields == fields
|
|
assert len(bt._fields) == len(bt._attrs)
|
|
return bt
|
|
|
|
def test_factory(self):
|
|
Point = BagType('Point', ('x', 'y'))
|
|
self.assertEqual(Point.__name__, 'Point')
|
|
self.assertEqual(Point.__slots__, ())
|
|
self.assertEqual(Point.__module__, __name__)
|
|
self.assertEqual(Point.__getitem__, tuple.__getitem__)
|
|
assert Point._fields == ('x', 'y')
|
|
assert Point._attrs == ('x', 'y')
|
|
|
|
self.assertRaises(ValueError, BagType, 'abc%', ('efg', 'ghi')) # type has non-alpha char
|
|
self.assertRaises(ValueError, BagType, 'class', ('efg', 'ghi')) # type has keyword
|
|
self.assertRaises(ValueError, BagType, '9abc', ('efg', 'ghi')) # type starts with digit
|
|
|
|
assert self._create('efg', 'g%hi')._attrs == ('efg', 'g_hi')
|
|
assert self._create('abc', 'class')._attrs == ('abc', '_class')
|
|
assert self._create('8efg', '9ghi')._attrs == ('_8efg', '_9ghi')
|
|
assert self._create('_efg', 'ghi')._attrs == ('_efg', 'ghi')
|
|
|
|
self.assertRaises(ValueError, BagType, 'abc', ('efg', 'efg', 'ghi')) # duplicate field
|
|
|
|
self._create('x1', 'y2', typename='Point0') # Verify that numbers are allowed in names
|
|
self._create('a', 'b', 'c', typename='_') # Test leading underscores in a typename
|
|
|
|
bt = self._create('a!', 'a?')
|
|
assert bt._attrs == ('a0', 'a1')
|
|
x = bt('foo', 'bar')
|
|
assert x.get('a!') == 'foo'
|
|
assert x.a0 == 'foo'
|
|
assert x.get('a?') == 'bar'
|
|
assert x.a1 == 'bar'
|
|
|
|
# check unicode output
|
|
bt = self._create('the', 'quick', 'brown', 'fox')
|
|
assert "u'" not in repr(bt._fields)
|
|
|
|
self.assertRaises(TypeError, Point._make, [11]) # catch too few args
|
|
self.assertRaises(TypeError, Point._make, [11, 22, 33]) # catch too many args
|
|
|
|
@unittest.skipIf(sys.flags.optimize >= 2, "Docstrings are omitted with -O2 and above")
|
|
def test_factory_doc_attr(self):
|
|
Point = BagType('Point', ('x', 'y'))
|
|
self.assertEqual(Point.__doc__, 'Point(x, y)')
|
|
|
|
@unittest.skipIf(sys.flags.optimize >= 2, "Docstrings are omitted with -O2 and above")
|
|
def test_doc_writable(self):
|
|
Point = BagType('Point', ('x', 'y'))
|
|
self.assertEqual(Point.x.__doc__, "Alias for 'x'")
|
|
Point.x.__doc__ = 'docstring for Point.x'
|
|
self.assertEqual(Point.x.__doc__, 'docstring for Point.x')
|
|
|
|
def test_name_fixer(self):
|
|
for spec, renamed in [
|
|
[('efg', 'g%hi'), ('efg', 'g_hi')], # field with non-alpha char
|
|
[('abc', 'class'), ('abc', '_class')], # field has keyword
|
|
[('8efg', '9ghi'), ('_8efg', '_9ghi')], # field starts with digit
|
|
[('abc', '_efg'), ('abc', '_efg')], # field with leading underscore
|
|
[('abc', '', 'x'), ('abc', '_0', 'x')], # fieldname is a space
|
|
[('&', '¨', '*'), ('_0', '_1', '_2')], # Duplicate attrs, in theory
|
|
]:
|
|
assert self._create(*spec)._attrs == renamed
|
|
|
|
def test_module_parameter(self):
|
|
NT = BagType('NT', ['x', 'y'], module=collections)
|
|
self.assertEqual(NT.__module__, collections)
|
|
|
|
def test_instance(self):
|
|
Point = self._create('x', 'y', typename='Point')
|
|
p = Point(11, 22)
|
|
self.assertEqual(p, Point(x=11, y=22))
|
|
self.assertEqual(p, Point(11, y=22))
|
|
self.assertEqual(p, Point(y=22, x=11))
|
|
self.assertEqual(p, Point(*(11, 22)))
|
|
self.assertEqual(p, Point(**dict(x=11, y=22)))
|
|
self.assertRaises(TypeError, Point, 1) # too few args
|
|
self.assertRaises(TypeError, Point, 1, 2, 3) # too many args
|
|
self.assertRaises(TypeError, eval, 'Point(XXX=1, y=2)', locals()) # wrong keyword argument
|
|
self.assertRaises(TypeError, eval, 'Point(x=1)', locals()) # missing keyword argument
|
|
self.assertEqual(repr(p), 'Point(x=11, y=22)')
|
|
self.assertNotIn('__weakref__', dir(p))
|
|
self.assertEqual(p, Point._make([11, 22])) # test _make classmethod
|
|
self.assertEqual(p._fields, ('x', 'y')) # test _fields attribute
|
|
self.assertEqual(p._replace(x=1), (1, 22)) # test _replace method
|
|
self.assertEqual(p._asdict(), dict(x=11, y=22)) # test _asdict method
|
|
|
|
try:
|
|
p._replace(x=1, error=2)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
self._fail('Did not detect an incorrect fieldname')
|
|
|
|
p = Point(x=11, y=22)
|
|
self.assertEqual(repr(p), 'Point(x=11, y=22)')
|
|
|
|
def test_tupleness(self):
|
|
Point = BagType('Point', ('x', 'y'))
|
|
p = Point(11, 22)
|
|
|
|
self.assertIsInstance(p, tuple)
|
|
self.assertEqual(p, (11, 22)) # matches a real tuple
|
|
self.assertEqual(tuple(p), (11, 22)) # coercable to a real tuple
|
|
self.assertEqual(list(p), [11, 22]) # coercable to a list
|
|
self.assertEqual(max(p), 22) # iterable
|
|
self.assertEqual(max(*p), 22) # star-able
|
|
x, y = p
|
|
self.assertEqual(p, (x, y)) # unpacks like a tuple
|
|
self.assertEqual((p[0], p[1]), (11, 22)) # indexable like a tuple
|
|
self.assertRaises(IndexError, p.__getitem__, 3)
|
|
|
|
self.assertEqual(p.x, x)
|
|
self.assertEqual(p.y, y)
|
|
self.assertRaises(AttributeError, eval, 'p.z', locals())
|
|
|
|
def test_odd_sizes(self):
|
|
Zero = BagType('Zero', ())
|
|
self.assertEqual(Zero(), ())
|
|
self.assertEqual(Zero._make([]), ())
|
|
self.assertEqual(repr(Zero()), 'Zero()')
|
|
self.assertEqual(Zero()._asdict(), {})
|
|
self.assertEqual(Zero()._fields, ())
|
|
|
|
Dot = BagType('Dot', ('d',))
|
|
self.assertEqual(Dot(1), (1,))
|
|
self.assertEqual(Dot._make([1]), (1,))
|
|
self.assertEqual(Dot(1).d, 1)
|
|
self.assertEqual(repr(Dot(1)), 'Dot(d=1)')
|
|
self.assertEqual(Dot(1)._asdict(), {'d': 1})
|
|
self.assertEqual(Dot(1)._replace(d=999), (999,))
|
|
self.assertEqual(Dot(1)._fields, ('d',))
|
|
|
|
n = 5000 if sys.version_info >= (3, 7) else 254
|
|
names = list(set(''.join([choice(string.ascii_letters) for j in range(10)]) for i in range(n)))
|
|
n = len(names)
|
|
Big = BagType('Big', names)
|
|
b = Big(*range(n))
|
|
self.assertEqual(b, tuple(range(n)))
|
|
self.assertEqual(Big._make(range(n)), tuple(range(n)))
|
|
for pos, name in enumerate(names):
|
|
self.assertEqual(getattr(b, name), pos)
|
|
repr(b) # make sure repr() doesn't blow-up
|
|
d = b._asdict()
|
|
d_expected = dict(zip(names, range(n)))
|
|
self.assertEqual(d, d_expected)
|
|
b2 = b._replace(**dict([(names[1], 999), (names[-5], 42)]))
|
|
b2_expected = list(range(n))
|
|
b2_expected[1] = 999
|
|
b2_expected[-5] = 42
|
|
self.assertEqual(b2, tuple(b2_expected))
|
|
self.assertEqual(b._fields, tuple(names))
|
|
|
|
def test_pickle(self):
|
|
p = TBag(x=10, y=20, z=30)
|
|
for module in (pickle,):
|
|
loads = getattr(module, 'loads')
|
|
dumps = getattr(module, 'dumps')
|
|
for protocol in range(-1, module.HIGHEST_PROTOCOL + 1):
|
|
q = loads(dumps(p, protocol))
|
|
self.assertEqual(p, q)
|
|
self.assertEqual(p._fields, q._fields)
|
|
self.assertNotIn(b'OrderedDict', dumps(p, protocol))
|
|
|
|
def test_copy(self):
|
|
p = TBag(x=10, y=20, z=30)
|
|
for copier in copy.copy, copy.deepcopy:
|
|
q = copier(p)
|
|
self.assertEqual(p, q)
|
|
self.assertEqual(p._fields, q._fields)
|
|
|
|
def test_name_conflicts(self):
|
|
# Some names like "self", "cls", "tuple", "itemgetter", and "property"
|
|
# failed when used as field names. Test to make sure these now work.
|
|
T = BagType('T', ('itemgetter', 'property', 'self', 'cls', 'tuple'))
|
|
t = T(1, 2, 3, 4, 5)
|
|
self.assertEqual(t, (1, 2, 3, 4, 5))
|
|
newt = t._replace(itemgetter=10, property=20, self=30, cls=40, tuple=50)
|
|
self.assertEqual(newt, (10, 20, 30, 40, 50))
|
|
|
|
# Broader test of all interesting names taken from the code, old
|
|
# template, and an example
|
|
words = {
|
|
'Alias',
|
|
'At',
|
|
'AttributeError',
|
|
'Build',
|
|
'Bypass',
|
|
'Create',
|
|
'Encountered',
|
|
'Expected',
|
|
'Field',
|
|
'For',
|
|
'Got',
|
|
'Helper',
|
|
'IronPython',
|
|
'Jython',
|
|
'KeyError',
|
|
'Make',
|
|
'Modify',
|
|
'Note',
|
|
'OrderedDict',
|
|
'Point',
|
|
'Return',
|
|
'Returns',
|
|
'Type',
|
|
'TypeError',
|
|
'Used',
|
|
'Validate',
|
|
'ValueError',
|
|
'Variables',
|
|
'a',
|
|
'accessible',
|
|
'add',
|
|
'added',
|
|
'all',
|
|
'also',
|
|
'an',
|
|
'arg_list',
|
|
'args',
|
|
'arguments',
|
|
'automatically',
|
|
'be',
|
|
'build',
|
|
'builtins',
|
|
'but',
|
|
'by',
|
|
'cannot',
|
|
'class_namespace',
|
|
'classmethod',
|
|
'cls',
|
|
'collections',
|
|
'convert',
|
|
'copy',
|
|
'created',
|
|
'creation',
|
|
'd',
|
|
'debugging',
|
|
'defined',
|
|
'dict',
|
|
'dictionary',
|
|
'doc',
|
|
'docstring',
|
|
'docstrings',
|
|
'duplicate',
|
|
'effect',
|
|
'either',
|
|
'enumerate',
|
|
'environments',
|
|
'error',
|
|
'example',
|
|
'exec',
|
|
'f',
|
|
'f_globals',
|
|
'field',
|
|
'field_names',
|
|
'fields',
|
|
'formatted',
|
|
'frame',
|
|
'function',
|
|
'functions',
|
|
'generate',
|
|
'getter',
|
|
'got',
|
|
'greater',
|
|
'has',
|
|
'help',
|
|
'identifiers',
|
|
'indexable',
|
|
'instance',
|
|
'instantiate',
|
|
'interning',
|
|
'introspection',
|
|
'isidentifier',
|
|
'isinstance',
|
|
'itemgetter',
|
|
'iterable',
|
|
'join',
|
|
'keyword',
|
|
'keywords',
|
|
'kwds',
|
|
'len',
|
|
'like',
|
|
'list',
|
|
'map',
|
|
'maps',
|
|
'message',
|
|
'metadata',
|
|
'method',
|
|
'methods',
|
|
'module',
|
|
'module_name',
|
|
'must',
|
|
'name',
|
|
'named',
|
|
'namedtuple',
|
|
'namedtuple_',
|
|
'names',
|
|
'namespace',
|
|
'needs',
|
|
'new',
|
|
'nicely',
|
|
'num_fields',
|
|
'number',
|
|
'object',
|
|
'of',
|
|
'operator',
|
|
'option',
|
|
'p',
|
|
'particular',
|
|
'pickle',
|
|
'pickling',
|
|
'plain',
|
|
'pop',
|
|
'positional',
|
|
'property',
|
|
'r',
|
|
'regular',
|
|
'rename',
|
|
'replace',
|
|
'replacing',
|
|
'repr',
|
|
'repr_fmt',
|
|
'representation',
|
|
'result',
|
|
'reuse_itemgetter',
|
|
's',
|
|
'seen',
|
|
'sequence',
|
|
'set',
|
|
'side',
|
|
'specified',
|
|
'split',
|
|
'start',
|
|
'startswith',
|
|
'step',
|
|
'str',
|
|
'string',
|
|
'strings',
|
|
'subclass',
|
|
'sys',
|
|
'targets',
|
|
'than',
|
|
'the',
|
|
'their',
|
|
'this',
|
|
'to',
|
|
'tuple_new',
|
|
'type',
|
|
'typename',
|
|
'underscore',
|
|
'unexpected',
|
|
'unpack',
|
|
'up',
|
|
'use',
|
|
'used',
|
|
'user',
|
|
'valid',
|
|
'values',
|
|
'variable',
|
|
'verbose',
|
|
'where',
|
|
'which',
|
|
'work',
|
|
'x',
|
|
'y',
|
|
'z',
|
|
'zip',
|
|
}
|
|
sorted_words = tuple(sorted(words))
|
|
T = BagType('T', sorted_words)
|
|
# test __new__
|
|
values = tuple(range(len(words)))
|
|
t = T(*values)
|
|
self.assertEqual(t, values)
|
|
t = T(**dict(zip(T._attrs, values)))
|
|
self.assertEqual(t, values)
|
|
# test _make
|
|
t = T._make(values)
|
|
self.assertEqual(t, values)
|
|
# exercise __repr__
|
|
repr(t)
|
|
# test _asdict
|
|
self.assertEqual(t._asdict(), dict(zip(T._fields, values)))
|
|
# test _replace
|
|
t = T._make(values)
|
|
newvalues = tuple(v * 10 for v in values)
|
|
newt = t._replace(**dict(zip(T._fields, newvalues)))
|
|
self.assertEqual(newt, newvalues)
|
|
# test _fields
|
|
self.assertEqual(T._attrs, sorted_words)
|
|
# test __getnewargs__
|
|
self.assertEqual(t.__getnewargs__(), values)
|
|
|
|
def test_repr(self):
|
|
A = BagType('A', ('x',))
|
|
self.assertEqual(repr(A(1)), 'A(x=1)')
|
|
|
|
# repr should show the name of the subclass
|
|
class B(A):
|
|
pass
|
|
|
|
self.assertEqual(repr(B(1)), 'B(x=1)')
|
|
|
|
def test_namedtuple_subclass_issue_24931(self):
|
|
class Point(BagType('_Point', ['x', 'y'])):
|
|
pass
|
|
|
|
a = Point(3, 4)
|
|
self.assertEqual(a._asdict(), OrderedDict([('x', 3), ('y', 4)]))
|
|
|
|
a.w = 5
|
|
self.assertEqual(a.__dict__, {'w': 5})
|
|
|
|
def test_annoying_attribute_names(self):
|
|
self._create(
|
|
'__slots__',
|
|
'__getattr__',
|
|
'_attrs',
|
|
'_fields',
|
|
'__new__',
|
|
'__getnewargs__',
|
|
'__repr__',
|
|
'_make',
|
|
'get',
|
|
'_replace',
|
|
'_asdict',
|
|
'_cls',
|
|
'self',
|
|
'tuple',
|
|
)
|