Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f4cb48a

Browse files
committed
Issue #4331: Added functools.partialmethod
Initial patch by Alon Horev
1 parent b19ff41 commit f4cb48a

5 files changed

Lines changed: 255 additions & 4 deletions

File tree

Doc/library/functools.rst

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,48 @@ The :mod:`functools` module defines the following functions:
194194
18
195195

196196

197+
.. class:: partialmethod(func, *args, **keywords)
198+
199+
Return a new :class:`partialmethod` descriptor which behaves
200+
like :class:`partial` except that it is designed to be used as a method
201+
definition rather than being directly callable.
202+
203+
*func* must be a :term:`descriptor` or a callable (objects which are both,
204+
like normal functions, are handled as descriptors).
205+
206+
When *func* is a descriptor (such as a normal Python function,
207+
:func:`classmethod`, :func:`staticmethod`, :func:`abstractmethod` or
208+
another instance of :class:`partialmethod`), calls to ``__get__`` are
209+
delegated to the underlying descriptor, and an appropriate
210+
:class:`partial` object returned as the result.
211+
212+
When *func* is a non-descriptor callable, an appropriate bound method is
213+
created dynamically. This behaves like a normal Python function when
214+
used as a method: the *self* argument will be inserted as the first
215+
positional argument, even before the *args* and *keywords* supplied to
216+
the :class:`partialmethod` constructor.
217+
218+
Example::
219+
220+
>>> class Cell(object):
221+
... @property
222+
... def alive(self):
223+
... return self._alive
224+
... def set_state(self, state):
225+
... self._alive = bool(state)
226+
... set_alive = partialmethod(set_alive, True)
227+
... set_dead = partialmethod(set_alive, False)
228+
...
229+
>>> c = Cell()
230+
>>> c.alive
231+
False
232+
>>> c.set_alive()
233+
>>> c.alive
234+
True
235+
236+
.. versionadded:: 3.4
237+
238+
197239
.. function:: reduce(function, iterable[, initializer])
198240

199241
Apply *function* of two arguments cumulatively to the items of *sequence*, from
@@ -431,4 +473,3 @@ differences. For instance, the :attr:`__name__` and :attr:`__doc__` attributes
431473
are not created automatically. Also, :class:`partial` objects defined in
432474
classes behave like static methods and do not transform into bound methods
433475
during instance attribute look-up.
434-

Doc/whatsnew/3.4.rst

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,25 @@ handling).
342342
functools
343343
---------
344344

345-
New :func:`functools.singledispatch` decorator: see the :pep:`443`.
345+
The new :func:`~functools.partialmethod` descriptor bring partial argument
346+
application to descriptors, just as :func:`~functools.partial` provides
347+
for normal callables. The new descriptor also makes it easier to get
348+
arbitrary callables (including :func:`~functools.partial` instances)
349+
to behave like normal instance methods when included in a class definition.
350+
351+
(Contributed by Alon Horev and Nick Coghlan in :issue:`4331`)
352+
353+
The new :func:`~functools.singledispatch` decorator brings support for
354+
single-dispatch generic functions to the Python standard library. Where
355+
object oriented programming focuses on grouping multiple operations on a
356+
common set of data into a class, a generic function focuses on grouping
357+
multiple implementations of an operation that allows it to work with
358+
*different* kinds of data.
359+
360+
.. seealso::
361+
362+
:pep:`443` - Single-dispatch generic functions
363+
PEP written and implemented by Łukasz Langa.
346364

347365

348366
hashlib

Lib/functools.py

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
pass
2020
from abc import get_cache_token
2121
from collections import namedtuple
22-
from types import MappingProxyType
22+
from types import MappingProxyType, MethodType
2323
from weakref import WeakKeyDictionary
2424
try:
2525
from _thread import RLock
@@ -223,8 +223,9 @@ def __ne__(self, other):
223223
### partial() argument application
224224
################################################################################
225225

226+
# Purely functional, no descriptor behaviour
226227
def partial(func, *args, **keywords):
227-
"""new function with partial application of the given arguments
228+
"""New function with partial application of the given arguments
228229
and keywords.
229230
"""
230231
def newfunc(*fargs, **fkeywords):
@@ -241,6 +242,79 @@ def newfunc(*fargs, **fkeywords):
241242
except ImportError:
242243
pass
243244

245+
# Descriptor version
246+
class partialmethod(object):
247+
"""Method descriptor with partial application of the given arguments
248+
and keywords.
249+
250+
Supports wrapping existing descriptors and handles non-descriptor
251+
callables as instance methods.
252+
"""
253+
254+
def __init__(self, func, *args, **keywords):
255+
if not callable(func) and not hasattr(func, "__get__"):
256+
raise TypeError("{!r} is not callable or a descriptor"
257+
.format(func))
258+
259+
# func could be a descriptor like classmethod which isn't callable,
260+
# so we can't inherit from partial (it verifies func is callable)
261+
if isinstance(func, partialmethod):
262+
# flattening is mandatory in order to place cls/self before all
263+
# other arguments
264+
# it's also more efficient since only one function will be called
265+
self.func = func.func
266+
self.args = func.args + args
267+
self.keywords = func.keywords.copy()
268+
self.keywords.update(keywords)
269+
else:
270+
self.func = func
271+
self.args = args
272+
self.keywords = keywords
273+
274+
def __repr__(self):
275+
args = ", ".join(map(repr, self.args))
276+
keywords = ", ".join("{}={!r}".format(k, v)
277+
for k, v in self.keywords.items())
278+
format_string = "{module}.{cls}({func}, {args}, {keywords})"
279+
return format_string.format(module=self.__class__.__module__,
280+
cls=self.__class__.__name__,
281+
func=self.func,
282+
args=args,
283+
keywords=keywords)
284+
285+
def _make_unbound_method(self):
286+
def _method(*args, **keywords):
287+
call_keywords = self.keywords.copy()
288+
call_keywords.update(keywords)
289+
cls_or_self, *rest = args
290+
call_args = (cls_or_self,) + self.args + tuple(rest)
291+
return self.func(*call_args, **call_keywords)
292+
_method.__isabstractmethod__ = self.__isabstractmethod__
293+
return _method
294+
295+
def __get__(self, obj, cls):
296+
get = getattr(self.func, "__get__", None)
297+
result = None
298+
if get is not None:
299+
new_func = get(obj, cls)
300+
if new_func is not self.func:
301+
# Assume __get__ returning something new indicates the
302+
# creation of an appropriate callable
303+
result = partial(new_func, *self.args, **self.keywords)
304+
try:
305+
result.__self__ = new_func.__self__
306+
except AttributeError:
307+
pass
308+
if result is None:
309+
# If the underlying descriptor didn't do anything, treat this
310+
# like an instance method
311+
result = self._make_unbound_method().__get__(obj, cls)
312+
return result
313+
314+
@property
315+
def __isabstractmethod__(self):
316+
return getattr(self.func, "__isabstractmethod__", False)
317+
244318

245319
################################################################################
246320
### LRU Cache function decorator

Lib/test/test_functools.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import abc
12
import collections
23
from itertools import permutations
34
import pickle
@@ -217,6 +218,120 @@ class TestPartialCSubclass(TestPartialC):
217218
partial = PartialSubclass
218219

219220

221+
class TestPartialMethod(unittest.TestCase):
222+
223+
class A(object):
224+
nothing = functools.partialmethod(capture)
225+
positional = functools.partialmethod(capture, 1)
226+
keywords = functools.partialmethod(capture, a=2)
227+
both = functools.partialmethod(capture, 3, b=4)
228+
229+
nested = functools.partialmethod(positional, 5)
230+
231+
over_partial = functools.partialmethod(functools.partial(capture, c=6), 7)
232+
233+
static = functools.partialmethod(staticmethod(capture), 8)
234+
cls = functools.partialmethod(classmethod(capture), d=9)
235+
236+
a = A()
237+
238+
def test_arg_combinations(self):
239+
self.assertEqual(self.a.nothing(), ((self.a,), {}))
240+
self.assertEqual(self.a.nothing(5), ((self.a, 5), {}))
241+
self.assertEqual(self.a.nothing(c=6), ((self.a,), {'c': 6}))
242+
self.assertEqual(self.a.nothing(5, c=6), ((self.a, 5), {'c': 6}))
243+
244+
self.assertEqual(self.a.positional(), ((self.a, 1), {}))
245+
self.assertEqual(self.a.positional(5), ((self.a, 1, 5), {}))
246+
self.assertEqual(self.a.positional(c=6), ((self.a, 1), {'c': 6}))
247+
self.assertEqual(self.a.positional(5, c=6), ((self.a, 1, 5), {'c': 6}))
248+
249+
self.assertEqual(self.a.keywords(), ((self.a,), {'a': 2}))
250+
self.assertEqual(self.a.keywords(5), ((self.a, 5), {'a': 2}))
251+
self.assertEqual(self.a.keywords(c=6), ((self.a,), {'a': 2, 'c': 6}))
252+
self.assertEqual(self.a.keywords(5, c=6), ((self.a, 5), {'a': 2, 'c': 6}))
253+
254+
self.assertEqual(self.a.both(), ((self.a, 3), {'b': 4}))
255+
self.assertEqual(self.a.both(5), ((self.a, 3, 5), {'b': 4}))
256+
self.assertEqual(self.a.both(c=6), ((self.a, 3), {'b': 4, 'c': 6}))
257+
self.assertEqual(self.a.both(5, c=6), ((self.a, 3, 5), {'b': 4, 'c': 6}))
258+
259+
self.assertEqual(self.A.both(self.a, 5, c=6), ((self.a, 3, 5), {'b': 4, 'c': 6}))
260+
261+
def test_nested(self):
262+
self.assertEqual(self.a.nested(), ((self.a, 1, 5), {}))
263+
self.assertEqual(self.a.nested(6), ((self.a, 1, 5, 6), {}))
264+
self.assertEqual(self.a.nested(d=7), ((self.a, 1, 5), {'d': 7}))
265+
self.assertEqual(self.a.nested(6, d=7), ((self.a, 1, 5, 6), {'d': 7}))
266+
267+
self.assertEqual(self.A.nested(self.a, 6, d=7), ((self.a, 1, 5, 6), {'d': 7}))
268+
269+
def test_over_partial(self):
270+
self.assertEqual(self.a.over_partial(), ((self.a, 7), {'c': 6}))
271+
self.assertEqual(self.a.over_partial(5), ((self.a, 7, 5), {'c': 6}))
272+
self.assertEqual(self.a.over_partial(d=8), ((self.a, 7), {'c': 6, 'd': 8}))
273+
self.assertEqual(self.a.over_partial(5, d=8), ((self.a, 7, 5), {'c': 6, 'd': 8}))
274+
275+
self.assertEqual(self.A.over_partial(self.a, 5, d=8), ((self.a, 7, 5), {'c': 6, 'd': 8}))
276+
277+
def test_bound_method_introspection(self):
278+
obj = self.a
279+
self.assertIs(obj.both.__self__, obj)
280+
self.assertIs(obj.nested.__self__, obj)
281+
self.assertIs(obj.over_partial.__self__, obj)
282+
self.assertIs(obj.cls.__self__, self.A)
283+
self.assertIs(self.A.cls.__self__, self.A)
284+
285+
def test_unbound_method_retrieval(self):
286+
obj = self.A
287+
self.assertFalse(hasattr(obj.both, "__self__"))
288+
self.assertFalse(hasattr(obj.nested, "__self__"))
289+
self.assertFalse(hasattr(obj.over_partial, "__self__"))
290+
self.assertFalse(hasattr(obj.static, "__self__"))
291+
self.assertFalse(hasattr(self.a.static, "__self__"))
292+
293+
def test_descriptors(self):
294+
for obj in [self.A, self.a]:
295+
with self.subTest(obj=obj):
296+
self.assertEqual(obj.static(), ((8,), {}))
297+
self.assertEqual(obj.static(5), ((8, 5), {}))
298+
self.assertEqual(obj.static(d=8), ((8,), {'d': 8}))
299+
self.assertEqual(obj.static(5, d=8), ((8, 5), {'d': 8}))
300+
301+
self.assertEqual(obj.cls(), ((self.A,), {'d': 9}))
302+
self.assertEqual(obj.cls(5), ((self.A, 5), {'d': 9}))
303+
self.assertEqual(obj.cls(c=8), ((self.A,), {'c': 8, 'd': 9}))
304+
self.assertEqual(obj.cls(5, c=8), ((self.A, 5), {'c': 8, 'd': 9}))
305+
306+
def test_overriding_keywords(self):
307+
self.assertEqual(self.a.keywords(a=3), ((self.a,), {'a': 3}))
308+
self.assertEqual(self.A.keywords(self.a, a=3), ((self.a,), {'a': 3}))
309+
310+
def test_invalid_args(self):
311+
with self.assertRaises(TypeError):
312+
class B(object):
313+
method = functools.partialmethod(None, 1)
314+
315+
def test_repr(self):
316+
self.assertEqual(repr(vars(self.A)['both']),
317+
'functools.partialmethod({}, 3, b=4)'.format(capture))
318+
319+
def test_abstract(self):
320+
class Abstract(abc.ABCMeta):
321+
322+
@abc.abstractmethod
323+
def add(self, x, y):
324+
pass
325+
326+
add5 = functools.partialmethod(add, 5)
327+
328+
self.assertTrue(Abstract.add.__isabstractmethod__)
329+
self.assertTrue(Abstract.add5.__isabstractmethod__)
330+
331+
for func in [self.A.static, self.A.cls, self.A.over_partial, self.A.nested, self.A.both]:
332+
self.assertFalse(getattr(func, '__isabstractmethod__', False))
333+
334+
220335
class TestUpdateWrapper(unittest.TestCase):
221336

222337
def check_wrapper(self, wrapper, wrapped,
@@ -1433,6 +1548,7 @@ def test_main(verbose=None):
14331548
TestPartialC,
14341549
TestPartialPy,
14351550
TestPartialCSubclass,
1551+
TestPartialMethod,
14361552
TestUpdateWrapper,
14371553
TestTotalOrdering,
14381554
TestCmpToKeyC,

Misc/NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,8 @@ Core and Builtins
11921192
Library
11931193
-------
11941194

1195+
- Issue #4331: Added functools.partialmethod (Initial patch by Alon Horev)
1196+
11951197
- Issue #13461: Fix a crash in the TextIOWrapper.tell method on 64-bit
11961198
platforms. Patch by Yogesh Chaudhari.
11971199

0 commit comments

Comments
 (0)