diff -r 377bd6e0f61c Lib/collections/__init__.py --- a/Lib/collections/__init__.py Mon Sep 09 22:40:13 2013 -0700 +++ b/Lib/collections/__init__.py Wed Sep 11 15:57:30 2013 +0300 @@ -1,5 +1,5 @@ __all__ = ['deque', 'defaultdict', 'namedtuple', 'UserDict', 'UserList', - 'UserString', 'Counter', 'OrderedDict', 'ChainMap'] + 'UserString', 'Counter', 'OrderedDict', 'ChainMap', 'TransformDict'] # For backwards compatibility, continue to make the collections ABCs # available through the collections module. @@ -861,6 +861,51 @@ self.maps[0].clear() +_sentinel = object() + + +class TransformDict(dict): + + __slots__ = ('_transform',) + + def __init__(self, transform, init_dict=None, **kwargs): + if not callable(transform): + raise TypeError("expected a callable, got %r" % transform.__class__) + self._transform = transform + if init_dict: + self.update(init_dict) + if kwargs: + self.update(kwargs) + + def __transform__(self, key): + return self._transform(key) + + @property + def transform(self): + return self._transform + + def copy(self): + other = self.__class__(self._transform) + other.update(self) + return other + + __copy__ = copy + + def __reduce__(self): + 'Return state information for pickle and deepcopy.' + if hasattr(self, '__dict__'): + inst_dict = self.__dict__.copy() + else: + inst_dict = None + return self.__class__, (self._transform,), inst_dict, None, iter(self.items()) + + def __repr__(self): + if not self: + return '%s(%r)' % (self.__class__.__name__, self._transform) + return '%s(%r, %s)' % (self.__class__.__name__, + self._transform, dict.__repr__(self)) + + ################################################################################ ### UserDict ################################################################################ diff -r 377bd6e0f61c Lib/test/test_collections.py --- a/Lib/test/test_collections.py Mon Sep 09 22:40:13 2013 -0700 +++ b/Lib/test/test_collections.py Wed Sep 11 15:57:30 2013 +0300 @@ -8,11 +8,13 @@ from test import mapping_tests import pickle, copy from random import randrange, shuffle +from functools import partial import keyword import re import sys from collections import UserDict from collections import ChainMap +from collections import TransformDict from collections.abc import Hashable, Iterable, Iterator from collections.abc import Sized, Container, Callable from collections.abc import Set, MutableSet @@ -1353,6 +1355,254 @@ self.assertRaises(KeyError, d.popitem) +def str_lower(s): + return s.lower() + +################################################################################ +### TransformDict +################################################################################ + +class TestTransformDict(unittest.TestCase): + + def check_underlying_dict(self, d, expected): + """ + Check for implementation details. + """ + return + self.assertEqual(set(d._data), set(expected)) + self.assertEqual({k: v[1] for k, v in d._data.items()}, expected) + #dict_iter = dict.__iter__ + #dict_getitem = dict.__getitem__ + #self.assertEqual(set(dict_iter(d)), set(expected)) + #self.assertEqual({k: dict_getitem(d, k)[1] for k in dict_iter(d)}, expected) + #self.assertEqual(dict.__len__(d), len(expected)) + + def test_init(self): + with self.assertRaises(TypeError): + TransformDict() + with self.assertRaises(TypeError): + # Too many positional args + TransformDict(str.lower, {}, {}) + d = TransformDict(str.lower) + self.check_underlying_dict(d, {}) + pairs = [('Bar', 1), ('Foo', 2)] + d = TransformDict(str.lower, pairs) + self.assertEqual(sorted(d.items()), pairs) + self.check_underlying_dict(d, {'bar': 1, 'foo': 2}) + d = TransformDict(str.lower, dict(pairs)) + self.assertEqual(sorted(d.items()), pairs) + self.check_underlying_dict(d, {'bar': 1, 'foo': 2}) + d = TransformDict(str.lower, **dict(pairs)) + self.assertEqual(sorted(d.items()), pairs) + self.check_underlying_dict(d, {'bar': 1, 'foo': 2}) + d = TransformDict(str.lower, {'Bar': 1}, Foo=2) + self.assertEqual(sorted(d.items()), pairs) + self.check_underlying_dict(d, {'bar': 1, 'foo': 2}) + + def test_various_transforms(self): + d = TransformDict(lambda s: s.encode('utf-8')) + d['Foo'] = 5 + self.assertEqual(d['Foo'], 5) + self.check_underlying_dict(d, {b'Foo': 5}) + with self.assertRaises(AttributeError): + # 'bytes' object has no attribute 'encode' + d[b'Foo'] + # Another example + d = TransformDict(str.swapcase) + d['Foo'] = 5 + self.assertEqual(d['Foo'], 5) + self.check_underlying_dict(d, {'fOO': 5}) + with self.assertRaises(KeyError): + d['fOO'] + + # NOTE: we only test the operations which are not inherited from + # MutableMapping. + + def test_setitem_getitem(self): + d = TransformDict(str.lower) + with self.assertRaises(KeyError): + d['foo'] + d['Foo'] = 5 + self.assertEqual(d['foo'], 5) + self.assertEqual(d['Foo'], 5) + self.assertEqual(d['FOo'], 5) + with self.assertRaises(KeyError): + d['bar'] + self.check_underlying_dict(d, {'foo': 5}) + d['BAR'] = 6 + self.assertEqual(d['Bar'], 6) + self.check_underlying_dict(d, {'foo': 5, 'bar': 6}) + # Overwriting + d['foO'] = 7 + self.assertEqual(d['foo'], 7) + self.assertEqual(d['Foo'], 7) + self.assertEqual(d['FOo'], 7) + self.check_underlying_dict(d, {'foo': 7, 'bar': 6}) + + def test_delitem(self): + d = TransformDict(str.lower, Foo=5) + d['baR'] = 3 + del d['fOO'] + with self.assertRaises(KeyError): + del d['Foo'] + with self.assertRaises(KeyError): + del d['foo'] + self.check_underlying_dict(d, {'bar': 3}) + + def test_get(self): + d = TransformDict(str.lower) + default = object() + self.assertIs(d.get('foo'), None) + self.assertIs(d.get('foo', default), default) + d['Foo'] = 5 + self.assertEqual(d.get('foo'), 5) + self.assertEqual(d.get('FOO'), 5) + self.assertIs(d.get('bar'), None) + self.check_underlying_dict(d, {'foo': 5}) + + def test_pop(self): + d = TransformDict(str.lower) + default = object() + with self.assertRaises(KeyError): + d.pop('foo') + self.assertIs(d.pop('foo', default), default) + d['Foo'] = 5 + self.assertIn('foo', d) + self.assertEqual(d.pop('foo'), 5) + self.assertNotIn('foo', d) + self.check_underlying_dict(d, {}) + d['Foo'] = 5 + self.assertIn('Foo', d) + self.assertEqual(d.pop('FOO'), 5) + self.assertNotIn('foo', d) + self.check_underlying_dict(d, {}) + with self.assertRaises(KeyError): + d.pop('foo') + + def test_clear(self): + d = TransformDict(str.lower) + d.clear() + self.check_underlying_dict(d, {}) + d['Foo'] = 5 + d['baR'] = 3 + self.check_underlying_dict(d, {'foo': 5, 'bar': 3}) + d.clear() + self.check_underlying_dict(d, {}) + + def test_contains(self): + d = TransformDict(str.lower) + self.assertIs(False, 'foo' in d) + d['Foo'] = 5 + self.assertIs(True, 'Foo' in d) + self.assertIs(True, 'foo' in d) + self.assertIs(True, 'FOO' in d) + self.assertIs(False, 'bar' in d) + + def test_len(self): + d = TransformDict(str.lower) + self.assertEqual(len(d), 0) + d['Foo'] = 5 + self.assertEqual(len(d), 1) + d['BAR'] = 6 + self.assertEqual(len(d), 2) + d['foo'] = 7 + self.assertEqual(len(d), 2) + d['baR'] = 3 + self.assertEqual(len(d), 2) + del d['Bar'] + self.assertEqual(len(d), 1) + + def test_iter(self): + d = TransformDict(str.lower) + it = iter(d) + with self.assertRaises(StopIteration): + next(it) + d['Foo'] = 5 + d['BAR'] = 6 + yielded = [] + for x in d: + yielded.append(x) + self.assertEqual(set(yielded), {'Foo', 'BAR'}) + + def test_repr(self): + d = TransformDict(str.lower) + self.assertEqual(repr(d), + "TransformDict()") + d['Foo'] = 5 + self.assertEqual(repr(d), + "TransformDict(, {'Foo': 5})") + d['Bar'] = 6 + if next(iter(d)) == 'Foo': + self.assertEqual(repr(d), + "TransformDict(, " + "{'Foo': 5, 'Bar': 6})") + else: + self.assertEqual(repr(d), + "TransformDict(, " + "{'Bar': 6, 'Foo': 5})") + + def check_shallow_copy(self, copy_func): + d = TransformDict(str_lower, {'Foo': []}) + e = copy_func(d) + self.assertIsInstance(e, TransformDict) + self.assertIs(e.transform, str_lower) + self.check_underlying_dict(e, {'foo': []}) + e['Bar'] = 6 + self.assertEqual(e['bar'], 6) + with self.assertRaises(KeyError): + d['bar'] + e['foo'].append(5) + self.assertEqual(d['foo'], [5]) + self.assertEqual(set(e), {'Foo', 'Bar'}) + + def check_deep_copy(self, copy_func): + d = TransformDict(str_lower, {'Foo': []}) + e = copy_func(d) + self.assertIsInstance(e, TransformDict) + self.assertIs(e.transform, str_lower) + self.check_underlying_dict(e, {'foo': []}) + e['Bar'] = 6 + self.assertEqual(e['bar'], 6) + with self.assertRaises(KeyError): + d['bar'] + e['foo'].append(5) + self.assertEqual(d['foo'], []) + self.check_underlying_dict(e, {'foo': [5], 'bar': 6}) + self.assertEqual(set(e), {'Foo', 'Bar'}) + + def test_copy(self): + self.check_shallow_copy(lambda d: d.copy()) + + def test_copy_copy(self): + self.check_shallow_copy(copy.copy) + + def test_cast_as_dict(self): + d = TransformDict(str.lower, {'Foo': 5}) + e = dict(d) + self.assertEqual(e, {'Foo': 5}) + + def test_copy_deepcopy(self): + self.check_deep_copy(copy.deepcopy) + + def test_pickling(self): + def pickle_unpickle(obj, proto): + data = pickle.dumps(obj, proto) + return pickle.loads(data) + for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(pickle_protocol=proto): + self.check_deep_copy(partial(pickle_unpickle, proto=proto)) + + +class TransformDictMappingTests(mapping_tests.BasicTestMappingProtocol): + type2test = partial(TransformDict, str.lower) + +class MyTransformDict(TransformDict): + pass + +class TransformDictSubclassMappingTests(mapping_tests.BasicTestMappingProtocol): + type2test = partial(MyTransformDict, str.lower) + + ################################################################################ ### Run tests ################################################################################ @@ -1363,7 +1613,9 @@ NamedTupleDocs = doctest.DocTestSuite(module=collections) test_classes = [TestNamedTuple, NamedTupleDocs, TestOneTrickPonyABCs, TestCollectionABCs, TestCounter, TestChainMap, - TestOrderedDict, GeneralMappingTests, SubclassMappingTests] + TestOrderedDict, GeneralMappingTests, SubclassMappingTests, + TestTransformDict, TransformDictMappingTests, + TransformDictSubclassMappingTests] support.run_unittest(*test_classes) support.run_doctest(collections, verbose) diff -r 377bd6e0f61c Objects/dictobject.c --- a/Objects/dictobject.c Mon Sep 09 22:40:13 2013 -0700 +++ b/Objects/dictobject.c Wed Sep 11 15:57:30 2013 +0300 @@ -218,6 +218,8 @@ Py_hash_t hash, PyObject ***value_addr); static PyDictKeyEntry *lookdict_split(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject ***value_addr); +static PyDictKeyEntry *lookdict_transform(PyDictObject *mp, PyObject *key, + Py_hash_t hash, PyObject ***value_addr); static int dictresize(PyDictObject *mp, Py_ssize_t minused); @@ -697,7 +699,8 @@ PyObject *key, *value; assert(PyDict_Check(dict)); /* Shortcut */ - if (((PyDictObject *)dict)->ma_keys->dk_lookup != lookdict) + if (((PyDictObject *)dict)->ma_keys->dk_lookup != lookdict && + ((PyDictObject *)dict)->ma_keys->dk_lookup != lookdict_transform) return 1; while (PyDict_Next(dict, &pos, &key, &value)) if (!PyUnicode_Check(key)) @@ -765,7 +768,8 @@ PyDictKeyEntry *ep; assert(key != NULL); - if (!PyUnicode_CheckExact(key)) + if (!PyUnicode_CheckExact(key) && + mp->ma_keys->dk_lookup != lookdict_transform) mp->ma_keys->dk_lookup = lookdict; i = hash & mask; ep = &ep0[i]; @@ -848,7 +852,9 @@ *value_addr = value; } assert(ep->me_key != NULL && ep->me_key != dummy); - assert(PyUnicode_CheckExact(key) || mp->ma_keys->dk_lookup == lookdict); + assert(PyUnicode_CheckExact(key) || + mp->ma_keys->dk_lookup == lookdict || + mp->ma_keys->dk_lookup == lookdict_transform); return 0; } @@ -877,7 +883,9 @@ assert(value != NULL); assert(key != NULL); assert(key != dummy); - assert(PyUnicode_CheckExact(key) || k->dk_lookup == lookdict); + assert(PyUnicode_CheckExact(key) || + k->dk_lookup == lookdict || + k->dk_lookup == lookdict_transform); i = hash & mask; ep = &ep0[i]; for (perturb = hash; ep->me_key != NULL; perturb >>= PERTURB_SHIFT) { @@ -890,6 +898,20 @@ ep->me_value = value; } +static int +dict_has_transform(PyObject *mp) +{ + if (!PyDict_CheckExact(mp)) { + _Py_IDENTIFIER(__transform__); + PyObject *transform = _PyObject_LookupSpecial(mp, &PyId___transform__); + if (transform) { + Py_DECREF(transform); + return 1; + } + } + return 0; +} + /* Restructure the table by allocating a new table and reinserting all items again. When entries have been deleted, the new table may @@ -925,12 +947,15 @@ mp->ma_keys = oldkeys; return -1; } - if (oldkeys->dk_lookup == lookdict) - mp->ma_keys->dk_lookup = lookdict; + if (oldkeys->dk_lookup == lookdict || + oldkeys->dk_lookup == lookdict_transform) + mp->ma_keys->dk_lookup = oldkeys->dk_lookup; oldsize = DK_SIZE(oldkeys); mp->ma_values = NULL; /* If empty then nothing to copy so just return */ if (oldsize == 1) { + if (dict_has_transform((PyObject *)mp)) + mp->ma_keys->dk_lookup = lookdict_transform; assert(oldkeys == Py_EMPTY_KEYS); DK_DECREF(oldkeys); return 0; @@ -1059,7 +1084,9 @@ if (!PyDict_Check(op)) return NULL; - if (!PyUnicode_CheckExact(key) || + if (mp->ma_keys->dk_lookup == lookdict_transform) + hash = (Py_hash_t)key; + else if (!PyUnicode_CheckExact(key) || (hash = ((PyASCIIObject *) key)->hash) == -1) { hash = PyObject_Hash(key); @@ -1112,7 +1139,9 @@ PyErr_BadInternalCall(); return NULL; } - if (!PyUnicode_CheckExact(key) || + if (mp->ma_keys->dk_lookup == lookdict_transform) + hash = (Py_hash_t)key; + else if (!PyUnicode_CheckExact(key) || (hash = ((PyASCIIObject *) key)->hash) == -1) { hash = PyObject_Hash(key); @@ -1190,7 +1219,9 @@ assert(key); assert(value); mp = (PyDictObject *)op; - if (!PyUnicode_CheckExact(key) || + if (mp->ma_keys->dk_lookup == lookdict_transform) + hash = (Py_hash_t)key; + else if (!PyUnicode_CheckExact(key) || (hash = ((PyASCIIObject *) key)->hash) == -1) { hash = PyObject_Hash(key); @@ -1216,13 +1247,15 @@ return -1; } assert(key); - if (!PyUnicode_CheckExact(key) || + mp = (PyDictObject *)op; + if (mp->ma_keys->dk_lookup == lookdict_transform) + hash = (Py_hash_t)key; + else if (!PyUnicode_CheckExact(key) || (hash = ((PyASCIIObject *) key)->hash) == -1) { hash = PyObject_Hash(key); if (hash == -1) return -1; } - mp = (PyDictObject *)op; ep = (mp->ma_keys->dk_lookup)(mp, key, hash, &value_addr); if (ep == NULL) return -1; @@ -1491,7 +1524,9 @@ PyDictKeyEntry *ep; PyObject **value_addr; - if (!PyUnicode_CheckExact(key) || + if (mp->ma_keys->dk_lookup == lookdict_transform) + hash = (Py_hash_t)key; + else if (!PyUnicode_CheckExact(key) || (hash = ((PyASCIIObject *) key)->hash) == -1) { hash = PyObject_Hash(key); if (hash == -1) @@ -1915,7 +1950,8 @@ return -1; } mp = (PyDictObject*)a; - if (PyDict_Check(b)) { + if (PyDict_Check(b) && !dict_has_transform(a) && + !dict_has_transform(b)) { other = (PyDictObject*)b; if (other == mp || other->ma_used == 0) /* a.update(a) or a.update({}); nothing to do */ @@ -2101,6 +2137,10 @@ /* can't be equal if # of entries differ */ return 0; /* Same # of entries -- check all of 'em. Exit early on any diff. */ + if (a->ma_keys->dk_lookup == lookdict_transform && + b->ma_keys->dk_lookup != lookdict_transform) { + PyDictObject *tmp = a; a = b; b = tmp; + } for (i = 0; i < DK_SIZE(a->ma_keys); i++) { PyDictKeyEntry *ep = &a->ma_keys->dk_entries[i]; PyObject *aval; @@ -2113,13 +2153,18 @@ PyObject *bval; PyObject **vaddr; PyObject *key = ep->me_key; + Py_hash_t hash; /* temporarily bump aval's refcount to ensure it stays alive until we're done with it */ Py_INCREF(aval); /* ditto for key */ Py_INCREF(key); /* reuse the known hash value */ - if ((b->ma_keys->dk_lookup)(b, key, ep->me_hash, &vaddr) == NULL) + if (b->ma_keys->dk_lookup != lookdict_transform) + hash = ep->me_hash; + else + hash = (Py_hash_t)key; + if ((b->ma_keys->dk_lookup)(b, key, hash, &vaddr) == NULL) bval = NULL; else bval = *vaddr; @@ -2167,7 +2212,9 @@ PyDictKeyEntry *ep; PyObject **value_addr; - if (!PyUnicode_CheckExact(key) || + if (mp->ma_keys->dk_lookup == lookdict_transform) + hash = (Py_hash_t)key; + else if (!PyUnicode_CheckExact(key) || (hash = ((PyASCIIObject *) key)->hash) == -1) { hash = PyObject_Hash(key); if (hash == -1) @@ -2192,7 +2239,9 @@ if (!PyArg_UnpackTuple(args, "get", 1, 2, &key, &failobj)) return NULL; - if (!PyUnicode_CheckExact(key) || + if (mp->ma_keys->dk_lookup == lookdict_transform) + hash = (Py_hash_t)key; + else if (!PyUnicode_CheckExact(key) || (hash = ((PyASCIIObject *) key)->hash) == -1) { hash = PyObject_Hash(key); if (hash == -1) @@ -2221,7 +2270,9 @@ PyErr_BadInternalCall(); return NULL; } - if (!PyUnicode_CheckExact(key) || + if (mp->ma_keys->dk_lookup == lookdict_transform) + hash = (Py_hash_t)key; + else if (!PyUnicode_CheckExact(key) || (hash = ((PyASCIIObject *) key)->hash) == -1) { hash = PyObject_Hash(key); if (hash == -1) @@ -2291,7 +2342,9 @@ _PyErr_SetKeyError(key); return NULL; } - if (!PyUnicode_CheckExact(key) || + if (mp->ma_keys->dk_lookup == lookdict_transform) + hash = (Py_hash_t)key; + else if (!PyUnicode_CheckExact(key) || (hash = ((PyASCIIObject *) key)->hash) == -1) { hash = PyObject_Hash(key); if (hash == -1) @@ -2538,7 +2591,9 @@ PyDictKeyEntry *ep; PyObject **value_addr; - if (!PyUnicode_CheckExact(key) || + if (mp->ma_keys->dk_lookup == lookdict_transform) + hash = (Py_hash_t)key; + else if (!PyUnicode_CheckExact(key) || (hash = ((PyASCIIObject *) key)->hash) == -1) { hash = PyObject_Hash(key); if (hash == -1) @@ -2596,6 +2651,9 @@ Py_DECREF(self); return NULL; } + if (d != NULL && d->ma_keys != Py_EMPTY_KEYS && + dict_has_transform((PyObject *)d)) + d->ma_keys->dk_lookup = lookdict_transform; return self; } @@ -3808,3 +3866,134 @@ 2, &PyDictDummy_Type }; +/* TransformDict implementation */ + +static PyDictKeyEntry * +lookdict_transform_helper(PyDictObject *mp, PyObject *key, PyObject *transform, + PyObject *tkey, Py_hash_t hash, PyObject ***value_addr) +{ + size_t i; + size_t perturb; + PyDictKeyEntry *freeslot; + size_t mask; + PyDictKeyEntry *ep0; + PyDictKeyEntry *ep; + int cmp; + PyObject *startkey, *tstartkey; + +top: + mask = DK_MASK(mp->ma_keys); + ep0 = &mp->ma_keys->dk_entries[0]; + i = (size_t)hash & mask; + ep = &ep0[i]; + if (ep->me_key == NULL || ep->me_key == key) { + *value_addr = &ep->me_value; + return ep; + } + if (ep->me_key == dummy) + freeslot = ep; + else { + /*if (ep->me_hash == hash)*/ { + startkey = ep->me_key; + Py_INCREF(startkey); + tstartkey = PyObject_CallFunctionObjArgs(transform, startkey, NULL); + if (tstartkey == NULL) + return NULL; + cmp = PyObject_RichCompareBool(tstartkey, tkey, Py_EQ); + Py_DECREF(tstartkey); + Py_DECREF(startkey); + if (cmp < 0) + return NULL; + if (ep0 == mp->ma_keys->dk_entries && ep->me_key == startkey) { + if (cmp > 0) { + *value_addr = &ep->me_value; + return ep; + } + } + else { + /* The dict was mutated, restart */ + goto top; + } + } + freeslot = NULL; + } + + /* In the loop, me_key == dummy is by far (factor of 100s) the + least likely outcome, so test for that last. */ + for (perturb = hash; ; perturb >>= PERTURB_SHIFT) { + i = (i << 2) + i + perturb + 1; + ep = &ep0[i & mask]; + if (ep->me_key == NULL) { + if (freeslot == NULL) { + *value_addr = &ep->me_value; + return ep; + } else { + *value_addr = &freeslot->me_value; + return freeslot; + } + } + if (ep->me_key == key) { + *value_addr = &ep->me_value; + return ep; + } + if (/*ep->me_hash == hash && */ep->me_key != dummy) { + startkey = ep->me_key; + Py_INCREF(startkey); + tstartkey = PyObject_CallFunctionObjArgs(transform, startkey, NULL); + if (tstartkey == NULL) { + *value_addr = NULL; + return NULL; + } + cmp = PyObject_RichCompareBool(startkey, tkey, Py_EQ); + Py_DECREF(tstartkey); + Py_DECREF(startkey); + if (cmp < 0) { + *value_addr = NULL; + return NULL; + } + if (ep0 == mp->ma_keys->dk_entries && ep->me_key == startkey) { + if (cmp > 0) { + *value_addr = &ep->me_value; + return ep; + } + } + else { + /* The dict was mutated, restart */ + goto top; + } + } + else if (ep->me_key == dummy && freeslot == NULL) + freeslot = ep; + } + assert(0); /* NOT REACHED */ + return 0; +} + +static PyDictKeyEntry * +lookdict_transform(PyDictObject *mp, PyObject *key, + Py_hash_t hash, PyObject ***value_addr) +{ + _Py_IDENTIFIER(__transform__); + PyObject *transform; + PyObject *tkey; + PyDictKeyEntry *ep; + + transform = _PyObject_LookupSpecial((PyObject *)mp, &PyId___transform__); + if (transform == NULL) + return NULL; + tkey = PyObject_CallFunctionObjArgs(transform, key, NULL); + if (tkey == NULL) { + Py_DECREF(transform); + return NULL; + } + hash = PyObject_Hash(tkey); + if (hash == -1) { + Py_DECREF(transform); + Py_DECREF(tkey); + return NULL; + } + ep = lookdict_transform_helper(mp, key, transform, tkey, hash, value_addr); + Py_DECREF(tkey); + Py_DECREF(transform); + return ep; +}