diff --git a/Lib/functools.py b/Lib/functools.py --- a/Lib/functools.py +++ b/Lib/functools.py @@ -237,26 +237,82 @@ except ImportError: ################################################################################ # Purely functional, no descriptor behaviour -def partial(func, *args, **keywords): +class partial: """New function with partial application of the given arguments and keywords. """ - if hasattr(func, 'func'): - args = func.args + args - tmpkw = func.keywords.copy() - tmpkw.update(keywords) - keywords = tmpkw - del tmpkw - func = func.func - - 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 + + def __new__(*args, **keywords): + if not args: + raise TypeError("descriptor '__new__' of partial needs an argument") + if len(args) < 2: + raise TypeError("type 'partial' takes at least one argument") + cls, func, *args = args + if not callable(func): + raise TypeError("the first argument must be callable") + args = tuple(args) + + if hasattr(func, "func"): + args = func.args + args + tmpkw = func.keywords.copy() + tmpkw.update(keywords) + keywords = tmpkw + del tmpkw + func = func.func + + self = super(partial, cls).__new__(cls) + + self.func = func + self.args = args + self.keywords = keywords + return self + + def __call__(*args, **keywords): + if not args: + raise TypeError("descriptor '__call__' of partial needs an argument") + self, *args = args + newkeywords = self.keywords.copy() + newkeywords.update(keywords) + return self.func(*self.args, *args, **newkeywords) + + def __repr__(self): + module = type(self).__module__ + qualname = type(self).__qualname__ + kwds = ", ".join(f"{k}={v!r}" for (k, v) in self.keywords.items()) + args = ", ".join(repr(x) for x in self.args) + if args and kwds: + res = ", ".join([args, kwds]) + elif args: + res = args + else: + res = kwds + if res: + result = ", ".join([str(self.func), res]) + else: + result = str(self.func) + return f"{module}.{qualname}({result})" + + def __reduce__(self): + return type(self), (self.func,), (self.func, self.args, + self.keywords, self.__dict__ or None) + + def __setstate__(self, state): + func, args, kwds, namespace = state + if (not callable(func) or not isinstance(args, tuple) or + (kwds is not None and not isinstance(kwds, dict))): + raise TypeError("invalid partial state") + + if type(args) is not tuple: + args = tuple(args) # coerce subclasses + if kwds is None: + kwds = {} + elif type(kwds) is not dict: + kwds = dict(kwds) + + self.func = func + self.args = args + self.keywords = kwds + self.__dict__ = namespace try: from _functools import partial diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -622,9 +622,10 @@ class TestWraps(TestUpdateWrapper): self.assertEqual(wrapper.attr, 'This is a different test') self.assertEqual(wrapper.dict_attr, f.dict_attr) - +@unittest.skipUnless(c_functools, 'requires the C _functools module') class TestReduce(unittest.TestCase): - func = functools.reduce + if c_functools: + func = c_functools.reduce def test_reduce(self): class Squares: