Skip to content

Commit 7b134d3

Browse files
authored
Descriptor HowTo: Update to include attributes added in Python 3.10 (pythonGH-103666)
1 parent b286295 commit 7b134d3

File tree

1 file changed

+90
-8
lines changed

1 file changed

+90
-8
lines changed

Doc/howto/descriptor.rst

+90-8
Original file line numberDiff line numberDiff line change
@@ -1273,37 +1273,86 @@ Using the non-data descriptor protocol, a pure Python version of
12731273

12741274
.. testcode::
12751275

1276+
import functools
1277+
12761278
class StaticMethod:
12771279
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
12781280

12791281
def __init__(self, f):
12801282
self.f = f
1283+
functools.update_wrapper(self, f)
12811284

12821285
def __get__(self, obj, objtype=None):
12831286
return self.f
12841287

12851288
def __call__(self, *args, **kwds):
12861289
return self.f(*args, **kwds)
12871290
1291+
The :func:`functools.update_wrapper` call adds a ``__wrapped__`` attribute
1292+
that refers to the underlying function. Also it carries forward
1293+
the attributes necessary to make the wrapper look like the wrapped
1294+
function: ``__name__``, ``__qualname__``, ``__doc__``, and ``__annotations__``.
1295+
12881296
.. testcode::
12891297
:hide:
12901298

12911299
class E_sim:
12921300
@StaticMethod
1293-
def f(x):
1294-
return x * 10
1301+
def f(x: int) -> str:
1302+
"Simple function example"
1303+
return "!" * x
12951304

12961305
wrapped_ord = StaticMethod(ord)
12971306

12981307
.. doctest::
12991308
:hide:
13001309

13011310
>>> E_sim.f(3)
1302-
30
1311+
'!!!'
13031312
>>> E_sim().f(3)
1304-
30
1313+
'!!!'
1314+
1315+
>>> sm = vars(E_sim)['f']
1316+
>>> type(sm).__name__
1317+
'StaticMethod'
1318+
>>> f = E_sim.f
1319+
>>> type(f).__name__
1320+
'function'
1321+
>>> sm.__name__
1322+
'f'
1323+
>>> f.__name__
1324+
'f'
1325+
>>> sm.__qualname__
1326+
'E_sim.f'
1327+
>>> f.__qualname__
1328+
'E_sim.f'
1329+
>>> sm.__doc__
1330+
'Simple function example'
1331+
>>> f.__doc__
1332+
'Simple function example'
1333+
>>> sm.__annotations__
1334+
{'x': <class 'int'>, 'return': <class 'str'>}
1335+
>>> f.__annotations__
1336+
{'x': <class 'int'>, 'return': <class 'str'>}
1337+
>>> sm.__module__ == f.__module__
1338+
True
1339+
>>> sm(3)
1340+
'!!!'
1341+
>>> f(3)
1342+
'!!!'
1343+
13051344
>>> wrapped_ord('A')
13061345
65
1346+
>>> wrapped_ord.__module__ == ord.__module__
1347+
True
1348+
>>> wrapped_ord.__wrapped__ == ord
1349+
True
1350+
>>> wrapped_ord.__name__ == ord.__name__
1351+
True
1352+
>>> wrapped_ord.__qualname__ == ord.__qualname__
1353+
True
1354+
>>> wrapped_ord.__doc__ == ord.__doc__
1355+
True
13071356

13081357

13091358
Class methods
@@ -1359,11 +1408,14 @@ Using the non-data descriptor protocol, a pure Python version of
13591408

13601409
.. testcode::
13611410

1411+
import functools
1412+
13621413
class ClassMethod:
13631414
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
13641415

13651416
def __init__(self, f):
13661417
self.f = f
1418+
functools.update_wrapper(self, f)
13671419

13681420
def __get__(self, obj, cls=None):
13691421
if cls is None:
@@ -1380,8 +1432,9 @@ Using the non-data descriptor protocol, a pure Python version of
13801432
# Verify the emulation works
13811433
class T:
13821434
@ClassMethod
1383-
def cm(cls, x, y):
1384-
return (cls, x, y)
1435+
def cm(cls, x: int, y: str) -> tuple[str, int, str]:
1436+
"Class method that returns a tuple"
1437+
return (cls.__name__, x, y)
13851438

13861439
@ClassMethod
13871440
@property
@@ -1393,17 +1446,40 @@ Using the non-data descriptor protocol, a pure Python version of
13931446
:hide:
13941447

13951448
>>> T.cm(11, 22)
1396-
(<class 'T'>, 11, 22)
1449+
('T', 11, 22)
13971450

13981451
# Also call it from an instance
13991452
>>> t = T()
14001453
>>> t.cm(11, 22)
1401-
(<class 'T'>, 11, 22)
1454+
('T', 11, 22)
14021455

14031456
# Check the alternate path for chained descriptors
14041457
>>> T.__doc__
14051458
"A doc for 'T'"
14061459

1460+
# Verify that T uses our emulation
1461+
>>> type(vars(T)['cm']).__name__
1462+
'ClassMethod'
1463+
1464+
# Verify that update_wrapper() correctly copied attributes
1465+
>>> T.cm.__name__
1466+
'cm'
1467+
>>> T.cm.__qualname__
1468+
'T.cm'
1469+
>>> T.cm.__doc__
1470+
'Class method that returns a tuple'
1471+
>>> T.cm.__annotations__
1472+
{'x': <class 'int'>, 'y': <class 'str'>, 'return': tuple[str, int, str]}
1473+
1474+
# Verify that __wrapped__ was added and works correctly
1475+
>>> f = vars(T)['cm'].__wrapped__
1476+
>>> type(f).__name__
1477+
'function'
1478+
>>> f.__name__
1479+
'cm'
1480+
>>> f(T, 11, 22)
1481+
('T', 11, 22)
1482+
14071483

14081484
The code path for ``hasattr(type(self.f), '__get__')`` was added in
14091485
Python 3.9 and makes it possible for :func:`classmethod` to support
@@ -1423,6 +1499,12 @@ chained together. In Python 3.11, this functionality was deprecated.
14231499
>>> G.__doc__
14241500
"A doc for 'G'"
14251501

1502+
The :func:`functools.update_wrapper` call in ``ClassMethod`` adds a
1503+
``__wrapped__`` attribute that refers to the underlying function. Also
1504+
it carries forward the attributes necessary to make the wrapper look
1505+
like the wrapped function: ``__name__``, ``__qualname__``, ``__doc__``,
1506+
and ``__annotations__``.
1507+
14261508

14271509
Member objects and __slots__
14281510
----------------------------

0 commit comments

Comments
 (0)