Index: py3k/Lib/test/test_functools.py =================================================================== --- py3k/Lib/test/test_functools.py (revision 81110) +++ py3k/Lib/test/test_functools.py (working copy) @@ -17,6 +17,38 @@ newfunc.keywords = keywords return newfunc +class PythonPartialCls: + + """Pure Python approximation of partial()""" + + def __init__(self, func, *args, **keywords): + self.func = func + self.args = args + self.keywords = keywords + + def __call__(self, *fargs, **fkeywords): + newkeywords = self.keywords.copy() + newkeywords.update(fkeywords) + return self.func(*(self.args + fargs), **newkeywords) + + def __eq__(self, other): + if isinstance(other, PythonPartialCls): + return ((self.func is other.func) and + (self.args == other.args) and + (self.keywords == other.keywords)) + else: + return NotImplemented + + def __hash__(self): + if self.keywords is None: + kwhash = None + else: + itemlist = list(self.keywords.items()) + itemlist.sort() + kwhash = tuple(itemlist) + return hash((self.func, self.args, kwhash)) + + def capture(*args, **kw): """capture all positional and keyword arguments""" return args, kw @@ -151,6 +183,32 @@ f_copy = pickle.loads(pickle.dumps(f)) self.assertEqual(signature(f), signature(f_copy)) + def test_equality(self): + f1 = self.thetype(capture, 1, {'a', 'b'}, a=2, b={'a': 1, 'b': 2}) + f2 = self.thetype(capture, 1, {'b', 'a'}, a=2, b={'b': 2, 'a': 1}) + self.assert_(f1 == f2) + self.assert_(not (f1 != f2)) + # func different: + f2 = self.thetype(signature, 1, {'b', 'a'}, a=2, b={'b': 2, 'a': 1}) + self.assert_(f1 != f2) + self.assert_(not (f1 == f2)) + # args different: + f2 = self.thetype(capture, 1, {'b', 'x'}, a=2, b={'b': 2, 'a': 1}) + self.assert_(f1 != f2) + self.assert_(not (f1 == f2)) + # kwargs different: + f2 = self.thetype(capture, 1, {'b', 'a'}, a=2, b={'b': 2, 'a': 9}) + self.assert_(f1 != f2) + self.assert_(not (f1 == f2)) + # == with some other object: + self.assert_(f1 != object()) + self.assert_(not (f1 == object())) + + def test_hash(self): + f1 = self.thetype(capture, 1, a=2, b=3) + f2 = self.thetype(capture, 1, b=3, a=2) + self.assertEqual(hash(f1), hash(f2)) + class PartialSubclass(functools.partial): pass @@ -165,6 +223,15 @@ # the python version isn't picklable def test_pickle(self): pass + # for eq and hash test we need to use the class implementation: + def test_equality(self): + self.thetype = PythonPartialCls + super().test_equality() + + def test_hash(self): + self.thetype = PythonPartialCls + super().test_equality() + class TestUpdateWrapper(unittest.TestCase): def check_wrapper(self, wrapper, wrapped, Index: py3k/Modules/_functoolsmodule.c =================================================================== --- py3k/Modules/_functoolsmodule.c (revision 81110) +++ py3k/Modules/_functoolsmodule.c (working copy) @@ -84,6 +84,50 @@ Py_TYPE(pto)->tp_free(pto); } +static long +partial_hash(partialobject *pto) +{ + PyObject *kwhash; + PyObject *itemlist; + PyObject *tuple; + long hash; + + if (pto->kw == Py_None) { + kwhash = Py_None; + Py_INCREF(Py_None); + } + /* pto->kw is a dict, but it isn't really mutable, + so we convert it to a sorted list of its items. */ + else { + /* Get the items as a list: */ + itemlist = PySequence_List(PyDict_Items(pto->kw)); + if(itemlist == NULL) + return -1; + + /* Sort it: */ + if (PyList_Sort(itemlist) == -1) { + Py_DECREF(itemlist); + return -1; + } + + /* Convert the sorted list to a tuple (because that is hashable): */ + kwhash = PyList_AsTuple(itemlist); + Py_DECREF(itemlist); + if (kwhash == NULL) + return -1; + } + + /* Return the hash of a tuple: (func, args, kwhash) */ + tuple = Py_BuildValue("(OOO)", pto->fn, pto->args, kwhash); + Py_DECREF(kwhash); + if (tuple == NULL) { + return -1; + } + hash = PyObject_Hash(tuple); + Py_DECREF(tuple); + return hash; +} + static PyObject * partial_call(partialobject *pto, PyObject *args, PyObject *kw) { @@ -140,6 +184,45 @@ return 0; } +static PyObject * +partial_richcompare(PyObject *left, PyObject *right, int op) +{ + int result; + partialobject *l_pto; + partialobject *r_pto; + + if (PyType_IsSubtype(Py_TYPE(right), &partial_type) && (op == Py_EQ || op == Py_NE)) { + l_pto = (partialobject *)left; + r_pto = (partialobject *)right; + if (l_pto->fn == r_pto->fn) { + result = PyObject_RichCompareBool(l_pto->args, r_pto->args, Py_EQ); + if (result == -1) + return NULL; + if (result == 1) + result = PyObject_RichCompareBool(l_pto->kw, r_pto->kw, Py_EQ); + if (result == -1) + return NULL; + } + else + result = 0; + + if (op == Py_NE) + result = (result == 0); + + if(result) { + Py_INCREF(Py_True); + return Py_True; + } + else { + Py_INCREF(Py_False); + return Py_False; + } + } + + Py_INCREF(Py_NotImplemented); + return Py_NotImplemented; +} + PyDoc_STRVAR(partial_doc, "partial(func, *args, **keywords) - new function with partial application\n\ of the given arguments and keywords.\n"); @@ -258,7 +341,7 @@ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ - 0, /* tp_hash */ + (hashfunc)partial_hash, /* tp_hash */ (ternaryfunc)partial_call, /* tp_call */ 0, /* tp_str */ PyObject_GenericGetAttr, /* tp_getattro */ @@ -269,7 +352,7 @@ partial_doc, /* tp_doc */ (traverseproc)partial_traverse, /* tp_traverse */ 0, /* tp_clear */ - 0, /* tp_richcompare */ + partial_richcompare, /* tp_richcompare */ offsetof(partialobject, weakreflist), /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */