diff -r 00db71b3c5bd Lib/functools.py --- a/Lib/functools.py Mon Jul 23 18:43:18 2012 +0200 +++ b/Lib/functools.py Tue Jul 24 17:43:12 2012 +1200 @@ -11,14 +11,12 @@ __all__ = ['update_wrapper', 'wraps', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES', 'total_ordering', 'cmp_to_key', 'lru_cache', 'reduce', 'partial'] -from _functools import partial, reduce from collections import namedtuple try: from _thread import allocate_lock as Lock except: from _dummy_thread import allocate_lock as Lock - ################################################################################ ### update_wrapper() and wraps() decorator ################################################################################ @@ -109,6 +107,79 @@ ### cmp_to_key() function converter ################################################################################ +class partial: + + def __init__(self, *args, **keywords): + """new function with partial application of the given arguments + and keywords. + """ + # support "func" as a keyword argument + try: + func, *args = args + args = tuple(args) + except ValueError: + raise TypeError("type 'partial' takes at least one argument") + + if not callable(func): + raise TypeError("the first argument must be callable") + + def newfunc(*fargs, **fkeywords): + newkeywords = keywords.copy() + newkeywords.update(fkeywords) + return func(*(args + fargs), **newkeywords) + + # create readonly attributes + self._func = func + self._args = args + self._keywords = keywords + + self.newfunc = newfunc + + @property + def func(self): + """function object to use in future partial calls""" + return self._func + + @property + def args(self): + """tuple of arguments to future partial calls""" + return self._args + + @property + def keywords(self): + """dictionary of keyword arguments to future partial calls""" + return self._keywords + + def __call__(self, *args, **kwargs): + return self.newfunc(*args, **kwargs) + + def __setattr__(self, name, value): + if hasattr(self, name): + raise AttributeError('readonly attribute') + super().__setattr__(name, value) + + def __delattr__(self, name): + raise TypeError('readonly attribute') + + def __repr__(self): + # positional arguments + arg_str = ', ' + ', '.join( + repr(arg) for arg in self._args) if len(self._args) else '' + + # keyword arguments + kwarg_str = ', ' + ', '.join( + "{}={}".format( + key, + self._keywords[key] + ) for key in self._keywords) if len(self._keywords) else '' + + return "partial({func}{args}{kwargs})".format( + func=repr(self._func), + args=arg_str, + kwargs=kwarg_str + ) + + def cmp_to_key(mycmp): """Convert a cmp= function into a key= function""" class K(object): @@ -130,11 +201,6 @@ __hash__ = None return K -try: - from _functools import cmp_to_key -except ImportError: - pass - ################################################################################ ### LRU Cache function decorator @@ -301,3 +367,8 @@ return update_wrapper(wrapper, user_function) return decorating_function + +try: + from _functools import partial, reduce, cmp_to_key +except ImportError: + pass diff -r 00db71b3c5bd Lib/test/test_functools.py --- a/Lib/test/test_functools.py Mon Jul 23 18:43:18 2012 +0200 +++ b/Lib/test/test_functools.py Tue Jul 24 17:43:12 2012 +1200 @@ -1,4 +1,3 @@ -import functools import collections import sys import unittest @@ -7,17 +6,31 @@ import pickle from random import choice -@staticmethod -def PythonPartial(func, *args, **keywords): - 'Pure Python approximation of partial()' - def newfunc(*fargs, **fkeywords): - newkeywords = keywords.copy() - newkeywords.update(fkeywords) - return func(*(args + fargs), **newkeywords) - newfunc.func = func - newfunc.args = args - newfunc.keywords = keywords - return newfunc +import functools + +original_functools = functools +py_functools = support.import_fresh_module('functools', blocked=['_functools']) +c_functools = support.import_fresh_module('functools', fresh=['_functools']) + +class BaseTest(unittest.TestCase): + + """Base class required for testing C and Py implementations.""" + + def setUp(self): + + # The module must be explicitly set so that the proper + # interaction between the c module and the python module + # can be controlled. + self.partial = self.module.partial + super(BaseTest, self).setUp() + +class BaseTestC(BaseTest): + module = c_functools + +class BaseTestPy(BaseTest): + module = py_functools + +PythonPartial = py_functools.partial def capture(*args, **kw): """capture all positional and keyword arguments""" @@ -27,31 +40,32 @@ """ return the signature of a partial object """ return (part.func, part.args, part.keywords, part.__dict__) -class TestPartial(unittest.TestCase): +class TestPartial(object): - thetype = functools.partial + partial = functools.partial def test_basic_examples(self): - p = self.thetype(capture, 1, 2, a=10, b=20) + p = self.partial(capture, 1, 2, a=10, b=20) + self.assertTrue(callable(p)) self.assertEqual(p(3, 4, b=30, c=40), ((1, 2, 3, 4), dict(a=10, b=30, c=40))) - p = self.thetype(map, lambda x: x*10) + p = self.partial(map, lambda x: x*10) self.assertEqual(list(p([1,2,3,4])), [10, 20, 30, 40]) def test_attributes(self): - p = self.thetype(capture, 1, 2, a=10, b=20) + p = self.partial(capture, 1, 2, a=10, b=20) # attributes should be readable self.assertEqual(p.func, capture) self.assertEqual(p.args, (1, 2)) self.assertEqual(p.keywords, dict(a=10, b=20)) # attributes should not be writable - if not isinstance(self.thetype, type): + if not isinstance(self.partial, type): return self.assertRaises(AttributeError, setattr, p, 'func', map) self.assertRaises(AttributeError, setattr, p, 'args', (1, 2)) self.assertRaises(AttributeError, setattr, p, 'keywords', dict(a=1, b=2)) - p = self.thetype(hex) + p = self.partial(hex) try: del p.__dict__ except TypeError: @@ -60,9 +74,9 @@ self.fail('partial object allowed __dict__ to be deleted') def test_argument_checking(self): - self.assertRaises(TypeError, self.thetype) # need at least a func arg + self.assertRaises(TypeError, self.partial) # need at least a func arg try: - self.thetype(2)() + self.partial(2)() except TypeError: pass else: @@ -73,7 +87,7 @@ def func(a=10, b=20): return a d = {'a':3} - p = self.thetype(func, a=5) + p = self.partial(func, a=5) self.assertEqual(p(**d), 3) self.assertEqual(d, {'a':3}) p(b=7) @@ -82,20 +96,20 @@ def test_arg_combinations(self): # exercise special code paths for zero args in either partial # object or the caller - p = self.thetype(capture) + p = self.partial(capture) self.assertEqual(p(), ((), {})) self.assertEqual(p(1,2), ((1,2), {})) - p = self.thetype(capture, 1, 2) + p = self.partial(capture, 1, 2) self.assertEqual(p(), ((1,2), {})) self.assertEqual(p(3,4), ((1,2,3,4), {})) def test_kw_combinations(self): # exercise special code paths for no keyword args in # either the partial object or the caller - p = self.thetype(capture) + p = self.partial(capture) self.assertEqual(p(), ((), {})) self.assertEqual(p(a=1), ((), {'a':1})) - p = self.thetype(capture, a=1) + p = self.partial(capture, a=1) self.assertEqual(p(), ((), {'a':1})) self.assertEqual(p(b=2), ((), {'a':1, 'b':2})) # keyword args in the call override those in the partial object @@ -104,7 +118,7 @@ def test_positional(self): # make sure positional arguments are captured correctly for args in [(), (0,), (0,1), (0,1,2), (0,1,2,3)]: - p = self.thetype(capture, *args) + p = self.partial(capture, *args) expected = args + ('x',) got, empty = p('x') self.assertTrue(expected == got and empty == {}) @@ -112,14 +126,14 @@ def test_keyword(self): # make sure keyword arguments are captured correctly for a in ['a', 0, None, 3.5]: - p = self.thetype(capture, a=a) + p = self.partial(capture, a=a) expected = {'a':a,'x':None} empty, got = p(x=None) self.assertTrue(expected == got and empty == ()) def test_no_side_effects(self): # make sure there are no side effects that affect subsequent calls - p = self.thetype(capture, 0, a=1) + p = self.partial(capture, 0, a=1) args1, kw1 = p(1, b=2) self.assertTrue(args1 == (0,1) and kw1 == {'a':1,'b':2}) args2, kw2 = p() @@ -128,13 +142,13 @@ def test_error_propagation(self): def f(x, y): x / y - self.assertRaises(ZeroDivisionError, self.thetype(f, 1, 0)) - self.assertRaises(ZeroDivisionError, self.thetype(f, 1), 0) - self.assertRaises(ZeroDivisionError, self.thetype(f), 1, 0) - self.assertRaises(ZeroDivisionError, self.thetype(f, y=0), 1) + self.assertRaises(ZeroDivisionError, self.partial(f, 1, 0)) + self.assertRaises(ZeroDivisionError, self.partial(f, 1), 0) + self.assertRaises(ZeroDivisionError, self.partial(f), 1, 0) + self.assertRaises(ZeroDivisionError, self.partial(f, y=0), 1) def test_weakref(self): - f = self.thetype(int, base=16) + f = self.partial(int, base=16) p = proxy(f) self.assertEqual(f.func, p.func) f = None @@ -142,9 +156,9 @@ def test_with_bound_and_unbound_methods(self): data = list(map(str, range(10))) - join = self.thetype(str.join, '') + join = self.partial(str.join, '') self.assertEqual(join(data), '0123456789') - join = self.thetype(''.join) + join = self.partial(''.join) self.assertEqual(join(data), '0123456789') def test_repr(self): @@ -152,49 +166,54 @@ args_repr = ', '.join(repr(a) for a in args) kwargs = {'a': object(), 'b': object()} kwargs_repr = ', '.join("%s=%r" % (k, v) for k, v in kwargs.items()) - if self.thetype is functools.partial: + if self.partial is functools.partial: name = 'functools.partial' else: - name = self.thetype.__name__ + name = self.partial.__name__ - f = self.thetype(capture) + f = self.partial(capture) self.assertEqual('{}({!r})'.format(name, capture), repr(f)) - f = self.thetype(capture, *args) + f = self.partial(capture, *args) self.assertEqual('{}({!r}, {})'.format(name, capture, args_repr), repr(f)) - f = self.thetype(capture, **kwargs) + f = self.partial(capture, **kwargs) self.assertEqual('{}({!r}, {})'.format(name, capture, kwargs_repr), repr(f)) - f = self.thetype(capture, *args, **kwargs) + f = self.partial(capture, *args, **kwargs) self.assertEqual('{}({!r}, {}, {})'.format(name, capture, args_repr, kwargs_repr), repr(f)) def test_pickle(self): - f = self.thetype(signature, 'asdf', bar=True) + f = self.partial(signature, 'asdf', bar=True) f.add_something_to__dict__ = True f_copy = pickle.loads(pickle.dumps(f)) self.assertEqual(signature(f), signature(f_copy)) -class PartialSubclass(functools.partial): +class TestPartialC(BaseTestC, TestPartial): pass -class TestPartialSubclass(TestPartial): +class TestPartialPy(BaseTestPy, TestPartial): - thetype = PartialSubclass + def test_pickle(self): + raise unittest.SkipTest("Python implementation of partial isn't picklable") -class TestPythonPartial(TestPartial): +class TestPartialCSubclass(BaseTestC, TestPartial): - thetype = PythonPartial + class PartialSubclass(c_functools.partial): + pass - # the python version hasn't a nice repr - def test_repr(self): pass + partial = staticmethod(PartialSubclass) - # the python version isn't picklable - def test_pickle(self): pass +class TestPartialPySubclass(TestPartialPy): + + class PartialSubclass(c_functools.partial): + pass + + partial = staticmethod(PartialSubclass) class TestUpdateWrapper(unittest.TestCase): @@ -441,24 +460,28 @@ d = {"one": 1, "two": 2, "three": 3} self.assertEqual(self.func(add, d), "".join(d.keys())) -class TestCmpToKey(unittest.TestCase): +class TestCmpToKey(object): def test_cmp_to_key(self): def cmp1(x, y): return (x > y) - (x < y) - key = functools.cmp_to_key(cmp1) + key = self.cmp_to_key(cmp1) self.assertEqual(key(3), key(3)) self.assertGreater(key(3), key(1)) + self.assertGreaterEqual(key(3), key(3)) + def cmp2(x, y): return int(x) - int(y) - key = functools.cmp_to_key(cmp2) + key = self.cmp_to_key(cmp2) self.assertEqual(key(4.0), key('4')) self.assertLess(key(2), key('35')) + self.assertLessEqual(key(2), key('35')) + self.assertNotEqual(key(2), key('35')) def test_cmp_to_key_arguments(self): def cmp1(x, y): return (x > y) - (x < y) - key = functools.cmp_to_key(mycmp=cmp1) + key = self.cmp_to_key(mycmp=cmp1) self.assertEqual(key(obj=3), key(obj=3)) self.assertGreater(key(obj=3), key(obj=1)) with self.assertRaises((TypeError, AttributeError)): @@ -466,10 +489,10 @@ with self.assertRaises((TypeError, AttributeError)): 1 < key(3) # lhs is not a K object with self.assertRaises(TypeError): - key = functools.cmp_to_key() # too few args + key = self.cmp_to_key() # too few args with self.assertRaises(TypeError): - key = functools.cmp_to_key(cmp1, None) # too many args - key = functools.cmp_to_key(cmp1) + key = self.module.cmp_to_key(cmp1, None) # too many args + key = self.cmp_to_key(cmp1) with self.assertRaises(TypeError): key() # too few args with self.assertRaises(TypeError): @@ -478,7 +501,7 @@ def test_bad_cmp(self): def cmp1(x, y): raise ZeroDivisionError - key = functools.cmp_to_key(cmp1) + key = self.cmp_to_key(cmp1) with self.assertRaises(ZeroDivisionError): key(3) > key(1) @@ -493,13 +516,13 @@ def test_obj_field(self): def cmp1(x, y): return (x > y) - (x < y) - key = functools.cmp_to_key(mycmp=cmp1) + key = self.cmp_to_key(mycmp=cmp1) self.assertEqual(key(50).obj, 50) def test_sort_int(self): def mycmp(x, y): return y - x - self.assertEqual(sorted(range(5), key=functools.cmp_to_key(mycmp)), + self.assertEqual(sorted(range(5), key=self.cmp_to_key(mycmp)), [4, 3, 2, 1, 0]) def test_sort_int_str(self): @@ -507,18 +530,24 @@ x, y = int(x), int(y) return (x > y) - (x < y) values = [5, '3', 7, 2, '0', '1', 4, '10', 1] - values = sorted(values, key=functools.cmp_to_key(mycmp)) + values = sorted(values, key=self.cmp_to_key(mycmp)) self.assertEqual([int(value) for value in values], [0, 1, 1, 2, 3, 4, 5, 7, 10]) def test_hash(self): def mycmp(x, y): return y - x - key = functools.cmp_to_key(mycmp) + key = self.cmp_to_key(mycmp) k = key(10) self.assertRaises(TypeError, hash, k) self.assertNotIsInstance(k, collections.Hashable) +class TestCmpToKeyC(BaseTestC, TestCmpToKey): + cmp_to_key = c_functools.cmp_to_key + +class TestCmpToKeyPy(BaseTestPy, TestCmpToKey): + cmp_to_key = staticmethod(py_functools.cmp_to_key) + class TestTotalOrdering(unittest.TestCase): def test_total_ordering_lt(self): @@ -623,7 +652,7 @@ def test_lru(self): def orig(x, y): - return 3*x+y + return 3 * x + y f = functools.lru_cache(maxsize=20)(orig) hits, misses, maxsize, currsize = f.cache_info() self.assertEqual(maxsize, 20) @@ -728,7 +757,7 @@ # Verify that user_function exceptions get passed through without # creating a hard-to-read chained exception. # http://bugs.python.org/issue13177 - for maxsize in (None, 100): + for maxsize in (None, 128): @functools.lru_cache(maxsize) def func(i): return 'abc'[i] @@ -741,7 +770,7 @@ func(15) def test_lru_with_types(self): - for maxsize in (None, 100): + for maxsize in (None, 128): @functools.lru_cache(maxsize=maxsize, typed=True) def square(x): return x * x @@ -756,14 +785,46 @@ self.assertEqual(square.cache_info().hits, 4) self.assertEqual(square.cache_info().misses, 4) + def test_lru_with_keyword_args(self): + @functools.lru_cache() + def fib(n): + if n < 2: + return n + return fib(n=n-1) + fib(n=n-2) + self.assertEqual( + [fib(n=number) for number in range(16)], + [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610] + ) + self.assertEqual(fib.cache_info(), + functools._CacheInfo(hits=28, misses=16, maxsize=128, currsize=16)) + fib.cache_clear() + self.assertEqual(fib.cache_info(), + functools._CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)) + + def test_lru_with_keyword_args_maxsize_none(self): + @functools.lru_cache(maxsize=None) + def fib(n): + if n < 2: + return n + return fib(n=n-1) + fib(n=n-2) + self.assertEqual([fib(n=number) for number in range(16)], + [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]) + self.assertEqual(fib.cache_info(), + functools._CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)) + fib.cache_clear() + self.assertEqual(fib.cache_info(), + functools._CacheInfo(hits=0, misses=0, maxsize=None, currsize=0)) + def test_main(verbose=None): test_classes = ( - TestPartial, - TestPartialSubclass, - TestPythonPartial, + TestPartialC, + TestPartialPy, + TestPartialCSubclass, + TestPartialPySubclass, TestUpdateWrapper, TestTotalOrdering, - TestCmpToKey, + TestCmpToKeyC, + TestCmpToKeyPy, TestWraps, TestReduce, TestLRU, diff -r 00db71b3c5bd Misc/ACKS --- a/Misc/ACKS Mon Jul 23 18:43:18 2012 +0200 +++ b/Misc/ACKS Tue Jul 24 17:43:12 2012 +1200 @@ -1037,6 +1037,7 @@ Nicolas M. ThiƩry James Thomas Robin Thomas +Brian Thorne Stephen Thorne Jeremy Thurgood Eric Tiedemann diff -r 00db71b3c5bd Misc/NEWS --- a/Misc/NEWS Mon Jul 23 18:43:18 2012 +0200 +++ b/Misc/NEWS Tue Jul 24 17:43:12 2012 +1200 @@ -759,6 +759,8 @@ - Issue #14753: Make multiprocessing's handling of negative timeouts the same as it was in Python 3.2. +- Issue #12428: Add pure python implementation of partial. + - Issue #14583: Fix importlib bug when a package's __init__.py would first import one of its modules then raise an error.