From 7111d70b6839656536adcfdc7e86d3d6e485311f Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 11:32:44 -0800 Subject: [PATCH 01/32] Test: Simple example --- Doc/howto/descriptor.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 8c6e90319a7f36..9cf7c7b003380f 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -43,21 +43,26 @@ Simple example: A descriptor that returns a constant ---------------------------------------------------- The :class:`Ten` class is a descriptor that always returns the constant ``10`` -from its :meth:`__get__` method:: +from its :meth:`__get__` method: +.. testcode:: class Ten: def __get__(self, obj, objtype=None): return 10 -To use the descriptor, it must be stored as a class variable in another class:: +To use the descriptor, it must be stored as a class variable in another class: + +.. testcode:: class A: x = 5 # Regular class attribute y = Ten() # Descriptor instance An interactive session shows the difference between normal attribute lookup -and descriptor lookup:: +and descriptor lookup: + +.. doctest:: >>> a = A() # Make an instance of class A >>> a.x # Normal attribute lookup From 46e08b6a45c995ddc2a5482529747cf0142de787 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 11:39:58 -0800 Subject: [PATCH 02/32] Test the rest of the primer section --- Doc/howto/descriptor.rst | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 9cf7c7b003380f..0c2837cc7638aa 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -88,7 +88,9 @@ Dynamic lookups --------------- Interesting descriptors typically run computations instead of returning -constants:: +constants: + +.. testcode:: import os @@ -136,7 +138,9 @@ the public attribute is accessed. In the following example, *age* is the public attribute and *_age* is the private attribute. When the public attribute is accessed, the descriptor logs -the lookup or update:: +the lookup or update: + +.. testcode:: import logging @@ -166,7 +170,9 @@ the lookup or update:: An interactive session shows that all access to the managed attribute *age* is -logged, but that the regular attribute *name* is not logged:: +logged, but that the regular attribute *name* is not logged: + +.. doctest:: >>> mary = Person('Mary M', 30) # The initial age update is logged INFO:root:Updating 'age' to 30 @@ -206,7 +212,9 @@ variable name was used. In this example, the :class:`Person` class has two descriptor instances, *name* and *age*. When the :class:`Person` class is defined, it makes a callback to :meth:`__set_name__` in *LoggedAccess* so that the field names can -be recorded, giving each descriptor its own *public_name* and *private_name*:: +be recorded, giving each descriptor its own *public_name* and *private_name*: + +.. testcode:: import logging @@ -241,14 +249,18 @@ be recorded, giving each descriptor its own *public_name* and *private_name*:: An interactive session shows that the :class:`Person` class has called :meth:`__set_name__` so that the field names would be recorded. Here -we call :func:`vars` to look up the descriptor without triggering it:: +we call :func:`vars` to look up the descriptor without triggering it: + +.. doctest:: >>> vars(vars(Person)['name']) {'public_name': 'name', 'private_name': '_name'} >>> vars(vars(Person)['age']) {'public_name': 'age', 'private_name': '_age'} -The new class now logs access to both *name* and *age*:: +The new class now logs access to both *name* and *age*: + +.. doctest:: >>> pete = Person('Peter P', 10) INFO:root:Updating 'name' to 'Peter P' @@ -257,7 +269,9 @@ The new class now logs access to both *name* and *age*:: INFO:root:Updating 'name' to 'Catherine C' INFO:root:Updating 'age' to 20 -The two *Person* instances contain only the private names:: +The two *Person* instances contain only the private names: + +.. doctest:: >>> vars(pete) {'_name': 'Peter P', '_age': 10} From 04fda8333e4800fbad8709346f5ace121e8e89ce Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 11:56:38 -0800 Subject: [PATCH 03/32] Doctest for the Validator example --- Doc/howto/descriptor.rst | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 0c2837cc7638aa..0d4c3e27490d21 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -326,7 +326,9 @@ restrictions. If those restrictions aren't met, it raises an exception to prevent data corruption at its source. This :class:`Validator` class is both an :term:`abstract base class` and a -managed attribute descriptor:: +managed attribute descriptor: + +.. testcode:: from abc import ABC, abstractmethod @@ -366,7 +368,7 @@ Here are three practical data validation utilities: user-defined `predicate `_ as well. -:: +.. testcode:: class OneOf(Validator): @@ -422,7 +424,9 @@ Here are three practical data validation utilities: Practical use ------------- -Here's how the data validators can be used in a real class:: +Here's how the data validators can be used in a real class: + +.. testcode:: class Component: @@ -437,11 +441,26 @@ Here's how the data validators can be used in a real class:: The descriptors prevent invalid instances from being created:: - Component('WIDGET', 'metal', 5) # Allowed. - Component('Widget', 'metal', 5) # Blocked: 'Widget' is not all uppercase - Component('WIDGET', 'metle', 5) # Blocked: 'metle' is misspelled - Component('WIDGET', 'metal', -5) # Blocked: -5 is negative - Component('WIDGET', 'metal', 'V') # Blocked: 'V' isn't a number + >>> Component('Widget', 'metal', 5) # Blocked: 'Widget' is not all uppercase + Traceback (most recent call last): + ... + ValueError: Expected to be true for 'Widget' + + >>> Component('WIDGET', 'metle', 5) # Blocked: 'metle' is misspelled + Traceback (most recent call last): + ... + ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'} + + >>> Component('WIDGET', 'metal', -5) # Blocked: -5 is negative + Traceback (most recent call last): + ... + ValueError: Expected -5 to be at least 0 + >>> Component('WIDGET', 'metal', 'V') # Blocked: 'V' isn't a number + Traceback (most recent call last): + ... + TypeError: Expected 'V' to be an int or float + + >>> c = Component('WIDGET', 'metal', 5) # Allowed: This inputs are valid Technical Tutorial From 101649269c2253749883dd9726fb7e4c4f558ac7 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 12:02:05 -0800 Subject: [PATCH 04/32] Fix typo --- Doc/howto/descriptor.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 0d4c3e27490d21..5b1766bf169e34 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -460,7 +460,7 @@ The descriptors prevent invalid instances from being created:: ... TypeError: Expected 'V' to be an int or float - >>> c = Component('WIDGET', 'metal', 5) # Allowed: This inputs are valid + >>> c = Component('WIDGET', 'metal', 5) # Allowed: The inputs are valid Technical Tutorial From c1201a43d3d68661b6b54fc35666b94cf122a976 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 12:13:19 -0800 Subject: [PATCH 05/32] Attempt to make logging doctestable --- Doc/howto/descriptor.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 5b1766bf169e34..ce8e2aec3cfc6e 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -172,6 +172,13 @@ the lookup or update: An interactive session shows that all access to the managed attribute *age* is logged, but that the regular attribute *name* is not logged: +.. testsetup:: + + import logging + import sys + + logging.basicConfig(stream=sys.stdout, force=True) + .. doctest:: >>> mary = Person('Mary M', 30) # The initial age update is logged @@ -260,6 +267,13 @@ we call :func:`vars` to look up the descriptor without triggering it: The new class now logs access to both *name* and *age*: +.. testsetup:: + + import logging + import sys + + logging.basicConfig(stream=sys.stdout, force=True) + .. doctest:: >>> pete = Person('Peter P', 10) From 81fd22c3b0c127b5624fd2bf34be3938d5363c3e Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 12:30:43 -0800 Subject: [PATCH 06/32] Test the ORM example --- Doc/howto/descriptor.rst | 52 ++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index ce8e2aec3cfc6e..27b76e789f8bd0 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -578,7 +578,9 @@ If a descriptor is found for ``a.x``, then it is invoked with: ``desc.__get__(a, type(a))``. The logic for a dotted lookup is in :meth:`object.__getattribute__`. Here is -a pure Python equivalent:: +a pure Python equivalent: + +.. testcode:: def object_getattribute(obj, name): "Emulate PyObject_GenericGetAttr() in Objects/object.c" @@ -600,7 +602,9 @@ a pure Python equivalent:: Interestingly, attribute lookup doesn't call :meth:`object.__getattribute__` directly. Instead, both the dot operator and the :func:`getattr` function -perform attribute lookup by way of a helper function:: +perform attribute lookup by way of a helper function: + +.. testcode:: def getattr_hook(obj, name): "Emulate slot_tp_getattr_hook() in Objects/typeobject.c" @@ -702,7 +706,9 @@ be used to implement an `object relational mapping The essential idea is that the data is stored in an external database. The Python instances only hold keys to the database's tables. Descriptors take -care of lookups or updates:: +care of lookups or updates: + +.. testcode:: class Field: @@ -717,8 +723,11 @@ care of lookups or updates:: conn.execute(self.store, [value, obj.key]) conn.commit() -We can use the :class:`Field` class to define "models" that describe the schema -for each table in a database:: +We can use the :class:`Field` class to define `models +`_ that describe the schema for +each table in a database: + +.. testcode:: class Movie: table = 'Movies' # Table name @@ -739,12 +748,41 @@ for each table in a database:: def __init__(self, key): self.key = key -An interactive session shows how data is retrieved from the database and how -it can be updated:: +To use the models, first connect to the database:: >>> import sqlite3 >>> conn = sqlite3.connect('entertainment.db') +An interactive session shows how data is retrieved from the database and how +it can be updated: + +.. testsetup:: + + song_data = [ + ('Country Roads', 'John Denver', 1972), + ('Me and Bobby McGee', 'Janice Joplin', 1971), + ('Coal Miners Daughter', 'Loretta Lynn', 1970), + ] + + movie_data = [ + ('Star Wars', 'George Lucas', 1977), + ('Jaws', 'Steven Spielberg', 1975), + ('Aliens', 'James Cameron', 1986), + ] + + import sqlite3 + + conn = sqlite3.connect(':memory:') + conn.execute('CREATE TABLE Music (title text, artist text, year integer);') + conn.execute('CREATE INDEX MusicNdx ON Music (title);') + conn.executemany('INSERT INTO Music VALUES (?, ?, ?);', song_data) + conn.execute('CREATE TABLE Movies (title text, director text, year integer);') + conn.execute('CREATE INDEX MovieNdx ON Music (title);') + conn.executemany('INSERT INTO Movies VALUES (?, ?, ?);', movie_data) + conn.commit() + +.. doctest:: + >>> Movie('Star Wars').director 'George Lucas' >>> jaws = Movie('Jaws') From c07d72977894c796b4f775fc60fda8e80c7b7199 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 12:44:54 -0800 Subject: [PATCH 07/32] Mock logging.info() --- Doc/howto/descriptor.rst | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 27b76e789f8bd0..4e7a73af6dca91 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -174,10 +174,11 @@ logged, but that the regular attribute *name* is not logged: .. testsetup:: - import logging - import sys - - logging.basicConfig(stream=sys.stdout, force=True) + class logging: + @staticmethod + def info(format_string, *args): + message = format_string % args + print('INFO:root:' + message) .. doctest:: @@ -269,10 +270,11 @@ The new class now logs access to both *name* and *age*: .. testsetup:: - import logging - import sys - - logging.basicConfig(stream=sys.stdout, force=True) + class logging: + @staticmethod + def info(format_string, *args): + message = format_string % args + print('INFO:root:' + message) .. doctest:: From 58666fa633255949912d8e44fc4ae998797b98c3 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 13:02:34 -0800 Subject: [PATCH 08/32] Test the remainder of the document --- Doc/howto/descriptor.rst | 79 ++++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 4e7a73af6dca91..4d29a124c31cfd 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -816,7 +816,9 @@ triggers a function call upon access to an attribute. Its signature is:: property(fget=None, fset=None, fdel=None, doc=None) -> property -The documentation shows a typical use to define a managed attribute ``x``:: +The documentation shows a typical use to define a managed attribute ``x``: + +.. testcode :: class C: def getx(self): return self.__x @@ -872,7 +874,9 @@ For instance, a spreadsheet class may grant access to a cell value through ``Cell('b10').value``. Subsequent improvements to the program require the cell to be recalculated on every access; however, the programmer does not want to affect existing client code accessing the attribute directly. The solution is -to wrap access to the value attribute in a property data descriptor:: +to wrap access to the value attribute in a property data descriptor: + +.. testcode :: class Cell: ... @@ -883,6 +887,9 @@ to wrap access to the value attribute in a property data descriptor:: self.recalc() return self._value +Either the built-in :func:`property` or our :func:`Property` equivalent would +work in this example. + Functions and methods --------------------- @@ -896,7 +903,9 @@ prepended to the other arguments. By convention, the instance is called *self* but could be called *this* or any other variable name. Methods can be created manually with :class:`types.MethodType` which is -roughly equivalent to:: +roughly equivalent to: + +.. testcode:: class MethodType: "Emulate Py_MethodType in Objects/classobject.c" @@ -913,7 +922,9 @@ roughly equivalent to:: To support automatic creation of methods, functions include the :meth:`__get__` method for binding methods during attribute access. This means that functions are non-data descriptors that return bound methods -during dotted lookup from an instance. Here's how it works:: +during dotted lookup from an instance. Here's how it works: + +.. testcode:: class Function: ... @@ -925,7 +936,9 @@ during dotted lookup from an instance. Here's how it works:: return MethodType(self, obj) Running the following class in the interpreter shows how the function -descriptor works in practice:: +descriptor works in practice: + +.. testcode:: class D: def f(self, x): @@ -933,6 +946,8 @@ descriptor works in practice:: The function has a :term:`qualified name` attribute to support introspection:: +.. doctest + >>> D.f.__qualname__ 'D.f' @@ -959,7 +974,7 @@ Internally, the bound method stores the underlying function and the bound instance:: >>> d.f.__func__ - + >>> d.f.__self__ <__main__.D object at 0x1012e1f98> @@ -1011,20 +1026,26 @@ It can be called either from an object or the class: ``s.erf(1.5) --> .9332`` o ``Sample.erf(1.5) --> .9332``. Since static methods return the underlying function with no changes, the -example calls are unexciting:: +example calls are unexciting: + +.. testcode:: class E: @staticmethod def f(x): print(x) +.. doctest:: + >>> E.f(3) 3 >>> E().f(3) 3 Using the non-data descriptor protocol, a pure Python version of -:func:`staticmethod` would look like this:: +:func:`staticmethod` would look like this: + +.. doctest:: class StaticMethod: "Emulate PyStaticMethod_Type() in Objects/funcobject.c" @@ -1057,7 +1078,9 @@ This behavior is useful whenever the method only needs to have a class reference and does rely on data stored in a specific instance. One use for class methods is to create alternate class constructors. For example, the classmethod :func:`dict.fromkeys` creates a new dictionary from a list of -keys. The pure Python equivalent is:: +keys. The pure Python equivalent is: + +.. testcode:: class Dict: ... @@ -1076,7 +1099,9 @@ Now a new dictionary of unique keys can be constructed like this:: {'a': None, 'r': None, 'b': None, 'c': None, 'd': None} Using the non-data descriptor protocol, a pure Python version of -:func:`classmethod` would look like this:: +:func:`classmethod` would look like this: + +.. testcode:: class ClassMethod: "Emulate PyClassMethod_Type() in Objects/funcobject.c" @@ -1093,7 +1118,9 @@ Using the non-data descriptor protocol, a pure Python version of The code path for ``hasattr(obj, '__get__')`` was added in Python 3.9 and makes it possible for :func:`classmethod` to support chained decorators. -For example, a classmethod and property could be chained together:: +For example, a classmethod and property could be chained together: + +.. testcode:: class G: @classmethod @@ -1121,7 +1148,9 @@ assignments. Only attribute names specified in ``__slots__`` are allowed:: AttributeError: 'Vehicle' object has no attribute 'id_nubmer' 2. Helps create immutable objects where descriptors manage access to private -attributes stored in ``__slots__``:: +attributes stored in ``__slots__``: + +.. testcode:: class Immutable: @@ -1169,7 +1198,9 @@ It's not possible to create an exact drop-in pure Python version of over object memory allocation. However, we can build a mostly faithful simulation where the actual C structure for slots is emulated by a private ``_slotvalues`` list. Reads and writes to that private structure are managed -by member descriptors:: +by member descriptors: + +.. testcode:: null = object() @@ -1208,6 +1239,8 @@ by member descriptors:: The :meth:`type.__new__` method takes care of adding member objects to class variables:: +.. testcode: + class Type(type): 'Simulate how the type metaclass adds member objects for slots' @@ -1221,7 +1254,9 @@ variables:: The :meth:`object.__new__` method takes care of creating instances that have slots instead of an instance dictionary. Here is a rough simulation in pure -Python:: +Python: + +.. testcode:: class Object: 'Simulate how object.__new__() allocates memory for __slots__' @@ -1253,7 +1288,9 @@ Python:: super().__delattr__(name) To use the simulation in a real class, just inherit from :class:`Object` and -set the :term:`metaclass` to :class:`Type`:: +set the :term:`metaclass` to :class:`Type`: + +.. testcode:: class H(Object, metaclass=Type): 'Instance variables stored in slots' @@ -1266,8 +1303,8 @@ set the :term:`metaclass` to :class:`Type`:: At this point, the metaclass has loaded member objects for *x* and *y*:: - >>> import pprint - >>> pprint.pp(dict(vars(H))) + >>> from pprint import pp + >>> pp(dict(vars(H))) {'__module__': '__main__', '__doc__': 'Instance variables stored in slots', 'slot_names': ['x', 'y'], @@ -1276,7 +1313,9 @@ At this point, the metaclass has loaded member objects for *x* and *y*:: 'y': } When instances are created, they have a ``slot_values`` list where the -attributes are stored:: +attributes are stored: + +.. doctest:: >>> h = H(10, 20) >>> vars(h) @@ -1285,7 +1324,9 @@ attributes are stored:: >>> vars(h) {'_slotvalues': [55, 20]} -Misspelled or unassigned attributes will raise an exception:: +Misspelled or unassigned attributes will raise an exception: + +.. doctest:: >>> h.xz Traceback (most recent call last): From 146961eadef0b59e03e3cdde3ddaf3ed50feb2b4 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 13:22:55 -0800 Subject: [PATCH 09/32] Fix markup --- Doc/howto/descriptor.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 4d29a124c31cfd..ee04a589b89c19 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1237,7 +1237,7 @@ by member descriptors: return f'' The :meth:`type.__new__` method takes care of adding member objects to class -variables:: +variables: .. testcode: From 719e7df5aed0c07f866e81878eca1405fc58e97d Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 13:30:45 -0800 Subject: [PATCH 10/32] Give-up on doctests for the logging examples --- Doc/howto/descriptor.rst | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index ee04a589b89c19..6910924ae6242d 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -170,17 +170,7 @@ the lookup or update: An interactive session shows that all access to the managed attribute *age* is -logged, but that the regular attribute *name* is not logged: - -.. testsetup:: - - class logging: - @staticmethod - def info(format_string, *args): - message = format_string % args - print('INFO:root:' + message) - -.. doctest:: +logged, but that the regular attribute *name* is not logged:: >>> mary = Person('Mary M', 30) # The initial age update is logged INFO:root:Updating 'age' to 30 @@ -266,17 +256,7 @@ we call :func:`vars` to look up the descriptor without triggering it: >>> vars(vars(Person)['age']) {'public_name': 'age', 'private_name': '_age'} -The new class now logs access to both *name* and *age*: - -.. testsetup:: - - class logging: - @staticmethod - def info(format_string, *args): - message = format_string % args - print('INFO:root:' + message) - -.. doctest:: +The new class now logs access to both *name* and *age*:: >>> pete = Person('Peter P', 10) INFO:root:Updating 'name' to 'Peter P' From 722c55118820b7bdb7aad331b1cf86aa07b3451f Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 13:33:46 -0800 Subject: [PATCH 11/32] Fix markup --- Doc/howto/descriptor.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 6910924ae6242d..adc9bbf446c587 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1219,7 +1219,7 @@ by member descriptors: The :meth:`type.__new__` method takes care of adding member objects to class variables: -.. testcode: +.. testcode:: class Type(type): 'Simulate how the type metaclass adds member objects for slots' From 076f7d6d75e53190bd5f4d1259629f4e4cc84414 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 14:02:45 -0800 Subject: [PATCH 12/32] Disable another non-doctestable block --- Doc/howto/descriptor.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index adc9bbf446c587..39fe8df169cda0 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -168,7 +168,6 @@ the lookup or update: def birthday(self): self.age += 1 # Calls both __get__() and __set__() - An interactive session shows that all access to the managed attribute *age* is logged, but that the regular attribute *name* is not logged:: @@ -265,9 +264,7 @@ The new class now logs access to both *name* and *age*:: INFO:root:Updating 'name' to 'Catherine C' INFO:root:Updating 'age' to 20 -The two *Person* instances contain only the private names: - -.. doctest:: +The two *Person* instances contain only the private names:: >>> vars(pete) {'_name': 'Peter P', '_age': 10} From d0dcd5c1605efd89871e8468ce6a18cbc094ddb7 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 15:03:55 -0800 Subject: [PATCH 13/32] More extensive tests for the Member() example. --- Doc/howto/descriptor.rst | 46 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 39fe8df169cda0..130f30a213c269 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1113,7 +1113,9 @@ fixed-length array of slot values. From a user point of view that has several effects: 1. Provides immediate detection of bugs due to misspelled attribute -assignments. Only attribute names specified in ``__slots__`` are allowed:: +assignments. Only attribute names specified in ``__slots__`` are allowed: + +.. doctest:: class Vehicle: __slots__ = ('id_number', 'make', 'model') @@ -1145,7 +1147,19 @@ attributes stored in ``__slots__``: def name(self): # Read-only descriptor return self._name - mark = Immutable('Botany', 'Mark Watney') # Create an immutable instance +.. doctest:: + + >>> mark = Immutable('Botany', 'Mark Watney') + >>> mark.dept + 'Botany' + >>> mark.dept = 'Space Pirate' + Traceback (most recent call last): + ... + AttributeError: can't set attribute + >>> mark.location = 'Mars' + Traceback (most recent call last): + ... + AttributeError: 'Immutable' object has no attribute 'location' 3. Saves memory. On a 64-bit Linux build, an instance with two attributes takes 48 bytes with ``__slots__`` and 152 bytes without. This `flyweight @@ -1153,7 +1167,9 @@ design pattern `_ likely only matters when a large number of instances are going to be created. 4. Blocks tools like :func:`functools.cached_property` which require an -instance dictionary to function correctly:: +instance dictionary to function correctly: + +.. doctest:: from functools import cached_property @@ -1289,6 +1305,13 @@ At this point, the metaclass has loaded member objects for *x* and *y*:: 'x': , 'y': } +.. testsetup:: + + # We test this separately because the preceding section is not + # doctestable due to the hex memory address for the __init__ function + assert isinstance(vars(H)['x'], Member) + assert isinstance(vars(H)['y'], Member) + When instances are created, they have a ``slot_values`` list where the attributes are stored: @@ -1309,3 +1332,20 @@ Misspelled or unassigned attributes will raise an exception: Traceback (most recent call last): ... AttributeError: 'H' object has no attribute 'xz' + +.. testsetup:: + + # Examples for deleted attributes are not shown because this section + # is already a bit lengthy. But we still test that code here. + del h.x + assert not hasattr(h, 'x') + + # Also test the code for uninitialized slots + class HU(Object, metaclass=Type): + slot_names = ['x', 'y'] + hu = HU() + assert not hasattr(hu, 'x') + assert not hasattr(hu, 'y') + + assert False, "verify doctest is catching these" + From 37ae72874f1dd651bf22cea8d155a6a0686b7ad4 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 15:06:19 -0800 Subject: [PATCH 14/32] Fix markup --- Doc/howto/descriptor.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 130f30a213c269..4e384f094a482e 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -795,7 +795,7 @@ triggers a function call upon access to an attribute. Its signature is:: The documentation shows a typical use to define a managed attribute ``x``: -.. testcode :: +.. testcode:: class C: def getx(self): return self.__x @@ -853,7 +853,7 @@ to be recalculated on every access; however, the programmer does not want to affect existing client code accessing the attribute directly. The solution is to wrap access to the value attribute in a property data descriptor: -.. testcode :: +.. testcode:: class Cell: ... @@ -923,7 +923,7 @@ descriptor works in practice: The function has a :term:`qualified name` attribute to support introspection:: -.. doctest +.. doctest:: >>> D.f.__qualname__ 'D.f' From 4a851ed82bbbb2c3b9b42b5bc68973cca432a59b Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 15:16:12 -0800 Subject: [PATCH 15/32] Doctest the classmethod section --- Doc/howto/descriptor.rst | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 4e384f094a482e..3200d55ca5cd07 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1039,7 +1039,9 @@ Class methods Unlike static methods, class methods prepend the class reference to the argument list before calling the function. This format is the same -for whether the caller is an object or a class:: +for whether the caller is an object or a class: + +.. doctest:: class F: @classmethod @@ -1059,9 +1061,7 @@ keys. The pure Python equivalent is: .. testcode:: - class Dict: - ... - + class Dict(dict): @classmethod def fromkeys(cls, iterable, value=None): "Emulate dict_fromkeys() in Objects/dictobject.c" @@ -1070,7 +1070,9 @@ keys. The pure Python equivalent is: d[key] = value return d -Now a new dictionary of unique keys can be constructed like this:: +Now a new dictionary of unique keys can be constructed like this: + +.. doctest:: >>> Dict.fromkeys('abracadabra') {'a': None, 'r': None, 'b': None, 'c': None, 'd': None} @@ -1105,6 +1107,12 @@ For example, a classmethod and property could be chained together: def __doc__(cls): return f'A doc for {cls.__name__!r}' +.. doctest:: + + >>> G().__doc__ + "A doc for 'G'" + + Member objects and __slots__ ---------------------------- From d2cc6b4dc61efde831284e81f6d8cec4ed4e6d12 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 15:24:31 -0800 Subject: [PATCH 16/32] Verify the ClassMethod() emulation --- Doc/howto/descriptor.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 3200d55ca5cd07..8d6d91b765d36f 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1095,6 +1095,19 @@ Using the non-data descriptor protocol, a pure Python version of return self.f.__get__(cls) return MethodType(self.f, cls) +.. testsetup:: + + # Verify the emulation works + class T: + @ClassMethod + def cm(cls, x, y): + return (cls, x, y) + assert T.cm(11, 22) == (T, 11, 22) + + # Also call it from an instance + t = T() + assert t.cm(11, 22) == (T, 11, 22) + The code path for ``hasattr(obj, '__get__')`` was added in Python 3.9 and makes it possible for :func:`classmethod` to support chained decorators. For example, a classmethod and property could be chained together: From 8c09cccf95a38a6b6a0629a559cc77b8cb68f98b Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 15:46:52 -0800 Subject: [PATCH 17/32] Add more tests for the Property() emulation --- Doc/howto/descriptor.rst | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 8d6d91b765d36f..88e59c8f9c7d56 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -414,8 +414,8 @@ Here are three practical data validation utilities: ) -Practical use -------------- +Practical application +--------------------- Here's how the data validators can be used in a real class: @@ -804,7 +804,9 @@ The documentation shows a typical use to define a managed attribute ``x``: x = property(getx, setx, delx, "I'm the 'x' property.") To see how :func:`property` is implemented in terms of the descriptor protocol, -here is a pure Python equivalent:: +here is a pure Python equivalent: + +.. testcode:: class Property: "Emulate PyProperty_Type() in Objects/descrobject.c" @@ -843,6 +845,36 @@ here is a pure Python equivalent:: def deleter(self, fdel): return type(self)(self.fget, self.fset, fdel, self.__doc__) +.. testsetup:: + + # Verify the Property() emulation code + class CC: + def getx(self): return self.__x + def setx(self, value): self.__x = value + def delx(self): del self.__x + x = Property(getx, setx, delx, "I'm the 'x' property.") + cc = CC() + assert not hasattr(cc, 'x') + cc.x = 33 + assert cc.x == 33 + del cc.x + assert not hasattr(cc, 'x') + + # Now do it again but the decorator style + class CCC: + @Property + def x(self): return self.__x + @x.setter + def x(self, value): self.__x = value + @x.deleter + def delx(self): del self.__x + ccc = CCC() + assert not hasattr(ccc, 'x') + ccc.x = 333 + assert ccc.x == 333 + del ccc.x + assert not hasattr(ccc, 'x') + The :func:`property` builtin helps whenever a user interface has granted attribute access and then subsequent changes require the intervention of a method. From a26495d7e2ea31863a0b7c9c6806d05ebf1f8906 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 15:50:58 -0800 Subject: [PATCH 18/32] See if the clean-up directive can run assertions --- Doc/howto/descriptor.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 88e59c8f9c7d56..46b7db32343ba4 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -845,7 +845,7 @@ here is a pure Python equivalent: def deleter(self, fdel): return type(self)(self.fget, self.fset, fdel, self.__doc__) -.. testsetup:: +.. testcleanup:: # Verify the Property() emulation code class CC: @@ -1127,7 +1127,7 @@ Using the non-data descriptor protocol, a pure Python version of return self.f.__get__(cls) return MethodType(self.f, cls) -.. testsetup:: +.. testcleanup:: # Verify the emulation works class T: @@ -1358,7 +1358,7 @@ At this point, the metaclass has loaded member objects for *x* and *y*:: 'x': , 'y': } -.. testsetup:: +.. testcleanup:: # We test this separately because the preceding section is not # doctestable due to the hex memory address for the __init__ function @@ -1386,7 +1386,7 @@ Misspelled or unassigned attributes will raise an exception: ... AttributeError: 'H' object has no attribute 'xz' -.. testsetup:: +.. testcleanup:: # Examples for deleted attributes are not shown because this section # is already a bit lengthy. But we still test that code here. From 961511c1f54613ba63e487b2af26952ef2d42044 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 16:07:57 -0800 Subject: [PATCH 19/32] Move cleanup sections to hidden testcode --- Doc/howto/descriptor.rst | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 46b7db32343ba4..a1161967254491 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -845,14 +845,14 @@ here is a pure Python equivalent: def deleter(self, fdel): return type(self)(self.fget, self.fset, fdel, self.__doc__) -.. testcleanup:: +.. testcode:: + :hide: - # Verify the Property() emulation code class CC: def getx(self): return self.__x def setx(self, value): self.__x = value def delx(self): del self.__x - x = Property(getx, setx, delx, "I'm the 'x' property.") + x = Property(getx, setx, delx, "I'm the 'x' property.") cc = CC() assert not hasattr(cc, 'x') cc.x = 33 @@ -1073,16 +1073,18 @@ Unlike static methods, class methods prepend the class reference to the argument list before calling the function. This format is the same for whether the caller is an object or a class: -.. doctest:: +.. testcode:: class F: @classmethod def f(cls, x): return cls.__name__, x - >>> print(F.f(3)) +.. doctest:: + + >>> F.f(3) ('F', 3) - >>> print(F().f(3)) + >>> F().f(3) ('F', 3) This behavior is useful whenever the method only needs to have a class @@ -1127,7 +1129,8 @@ Using the non-data descriptor protocol, a pure Python version of return self.f.__get__(cls) return MethodType(self.f, cls) -.. testcleanup:: +.. testcode:: + :hide: # Verify the emulation works class T: @@ -1358,7 +1361,8 @@ At this point, the metaclass has loaded member objects for *x* and *y*:: 'x': , 'y': } -.. testcleanup:: +.. testcode:: + :hide: # We test this separately because the preceding section is not # doctestable due to the hex memory address for the __init__ function @@ -1386,7 +1390,8 @@ Misspelled or unassigned attributes will raise an exception: ... AttributeError: 'H' object has no attribute 'xz' -.. testcleanup:: +.. testcode:: + :hide: # Examples for deleted attributes are not shown because this section # is already a bit lengthy. But we still test that code here. From 53bd1d15d0815b2e3a4ff8f87a35bcf7d508d5ca Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 16:09:48 -0800 Subject: [PATCH 20/32] Fix key ordering in the fromkeys() example. --- Doc/howto/descriptor.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index a1161967254491..d8e497f1f7b0fb 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1109,7 +1109,7 @@ Now a new dictionary of unique keys can be constructed like this: .. doctest:: >>> Dict.fromkeys('abracadabra') - {'a': None, 'r': None, 'b': None, 'c': None, 'd': None} + {'a': None, 'b': None, 'r': None, 'c': None, 'd': None} Using the non-data descriptor protocol, a pure Python version of :func:`classmethod` would look like this: From 979e10f8f7262eea8df29710249179d3db46d2c7 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 16:11:31 -0800 Subject: [PATCH 21/32] Separate the Vehicle testcode from its doctest --- Doc/howto/descriptor.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index d8e497f1f7b0fb..b67853ed3cc43a 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1171,11 +1171,13 @@ several effects: 1. Provides immediate detection of bugs due to misspelled attribute assignments. Only attribute names specified in ``__slots__`` are allowed: -.. doctest:: +.. testcode:: class Vehicle: __slots__ = ('id_number', 'make', 'model') +.. doctest:: + >>> auto = Vehicle() >>> auto.id_nubmer = 'VYE483814LQEX' Traceback (most recent call last): From 3a7fe082c0815d4575838d5f01450ad180fe0c3f Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 16:13:11 -0800 Subject: [PATCH 22/32] Separate CP testcode from its doctest --- Doc/howto/descriptor.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index b67853ed3cc43a..8d0aaf0b2e33e4 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1227,8 +1227,8 @@ matters when a large number of instances are going to be created. 4. Blocks tools like :func:`functools.cached_property` which require an instance dictionary to function correctly: -.. doctest:: - +.. testcode:: + from functools import cached_property class CP: @@ -1239,6 +1239,8 @@ instance dictionary to function correctly: return 4 * sum((-1.0)**n / (2.0*n + 1.0) for n in reversed(range(100_000))) +.. doctest:: + >>> CP().pi Traceback (most recent call last): ... From bc5c2a68cf0586ff0d738073dfccc61537a2c1b5 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 16:33:12 -0800 Subject: [PATCH 23/32] Fix spacing --- Doc/howto/descriptor.rst | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 8d0aaf0b2e33e4..d9cf49e34fa5a7 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -848,11 +848,14 @@ here is a pure Python equivalent: .. testcode:: :hide: + # Verify the Property() emulation + class CC: def getx(self): return self.__x def setx(self, value): self.__x = value def delx(self): del self.__x - x = Property(getx, setx, delx, "I'm the 'x' property.") + x = Property(getx, setx, delx, "I'm the 'x' property.") + cc = CC() assert not hasattr(cc, 'x') cc.x = 33 @@ -861,6 +864,7 @@ here is a pure Python equivalent: assert not hasattr(cc, 'x') # Now do it again but the decorator style + class CCC: @Property def x(self): return self.__x @@ -868,6 +872,7 @@ here is a pure Python equivalent: def x(self, value): self.__x = value @x.deleter def delx(self): del self.__x + ccc = CCC() assert not hasattr(ccc, 'x') ccc.x = 333 @@ -1228,7 +1233,7 @@ matters when a large number of instances are going to be created. instance dictionary to function correctly: .. testcode:: - + from functools import cached_property class CP: @@ -1239,7 +1244,7 @@ instance dictionary to function correctly: return 4 * sum((-1.0)**n / (2.0*n + 1.0) for n in reversed(range(100_000))) -.. doctest:: +.. doctest:: >>> CP().pi Traceback (most recent call last): @@ -1397,17 +1402,14 @@ Misspelled or unassigned attributes will raise an exception: .. testcode:: :hide: - # Examples for deleted attributes are not shown because this section - # is already a bit lengthy. But we still test that code here. - del h.x - assert not hasattr(h, 'x') - - # Also test the code for uninitialized slots - class HU(Object, metaclass=Type): - slot_names = ['x', 'y'] - hu = HU() - assert not hasattr(hu, 'x') - assert not hasattr(hu, 'y') - - assert False, "verify doctest is catching these" - + # Examples for deleted attributes are not shown because this section + # is already a bit lengthy. But we still test that code here. + del h.x + assert not hasattr(h, 'x') + + # Also test the code for uninitialized slots + class HU(Object, metaclass=Type): + slot_names = ['x', 'y'] + hu = HU() + assert not hasattr(hu, 'x') + assert not hasattr(hu, 'y') From fff2ef03a44554aac9bd6e6b6b3d3588189fbc87 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 16:36:35 -0800 Subject: [PATCH 24/32] Remove contraction --- Doc/howto/descriptor.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index d9cf49e34fa5a7..6045e4a403deee 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1251,7 +1251,7 @@ instance dictionary to function correctly: ... TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property. -It's not possible to create an exact drop-in pure Python version of +It is not possible to create an exact drop-in pure Python version of ``__slots__`` because it requires direct access to C structures and control over object memory allocation. However, we can build a mostly faithful simulation where the actual C structure for slots is emulated by a private From 0c819d0d2bb44951213eea12c25ed9cf20295202 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 17:13:49 -0800 Subject: [PATCH 25/32] Fix markup --- Doc/howto/descriptor.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 6045e4a403deee..0ef6fed6a2ee38 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -958,7 +958,7 @@ descriptor works in practice: def f(self, x): return x -The function has a :term:`qualified name` attribute to support introspection:: +The function has a :term:`qualified name` attribute to support introspection: .. doctest:: From c66d2e0da0b353cda600585efc363cbea41ab293 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 17:16:18 -0800 Subject: [PATCH 26/32] Nicer code alignment --- Doc/howto/descriptor.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 0ef6fed6a2ee38..b3ef9350caeedc 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -851,9 +851,12 @@ here is a pure Python equivalent: # Verify the Property() emulation class CC: - def getx(self): return self.__x - def setx(self, value): self.__x = value - def delx(self): del self.__x + def getx(self): + return self.__x + def setx(self, value): + self.__x = value + def delx(self): + del self.__x x = Property(getx, setx, delx, "I'm the 'x' property.") cc = CC() From 5eaddc379eb3b4fb943ebc52356113a6f3bbc221 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 17:54:38 -0800 Subject: [PATCH 27/32] Fix alignment of the :hide: option --- Doc/howto/descriptor.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index b3ef9350caeedc..8d5e0020331842 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -846,7 +846,7 @@ here is a pure Python equivalent: return type(self)(self.fget, self.fset, fdel, self.__doc__) .. testcode:: - :hide: + :hide: # Verify the Property() emulation @@ -870,11 +870,14 @@ here is a pure Python equivalent: class CCC: @Property - def x(self): return self.__x + def x(self): + return self.__x @x.setter - def x(self, value): self.__x = value + def x(self, value): + self.__x = value @x.deleter - def delx(self): del self.__x + def delx(self): + del self.__x ccc = CCC() assert not hasattr(ccc, 'x') From 5cdfbdcbcef49f509bae1d1ea71311e67c311292 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 18:49:24 -0800 Subject: [PATCH 28/32] Test object_getattribute() --- Doc/howto/descriptor.rst | 80 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 8d5e0020331842..6ef01292299888 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -600,6 +600,84 @@ raises :exc:`AttributeError` (either directly or in one of the descriptor calls) Also, if a user calls :meth:`object.__getattribute__` directly, the :meth:`__getattr__` hook is bypassed entirely. +.. testcode: + :hide: + + class Object: + "Dotted access uses normal __getattribute__. Square brackets uses the emulation" + + def __getitem__(obj, name): + try: + return object_getattribute(obj, name) + except AttributeError: + if not hasattr(type(obj), '__getattr__'): + raise + return type(obj).__getattr__(obj, name) # __getattr__ + + class A(Object): + + x = 10 + + def __init__(self, z): + self.z = z + + @property + def p2(self): + return 2 * self.x + + @property + def p3(self): + return 3 * self.x + + def m5(self, y): + return 5 * y + + def m7(self, y): + return 7 * y + + def __getattr__(self, name): + return ('getattr_hook', self, name) + + class B: + + __getitem__ = Object.__getitem__ + + __slots__ = ['z'] + + x = 15 + + def __init__(self, z): + self.z = z + + @property + def p2(self): + return 2 * self.x + + def m5(self, y): + return 5 * y + + def __getattr__(self, name): + return ('getattr_hook', self, name) + + + a = A(11) + vars(a).update(p3 = '_p3', m7 = '_m7') + assert a.x == a['x'] == 10 + assert a.z == a['z'] == 11 + assert a.p2 == a['p2'] == 20 + assert a.p3 == a['p3'] == 30 + assert a.m5(100) == a.m5(100) == 500 + assert a.m7 == a['m7'] == '_m7' + assert a.g == a['g'] == ('getattr_hook', a, 'g') + + b = B(22) + assert b.x == b['x'] == 15 + assert b.z == b['z'] == 22 + assert b.p2 == b['p2'] == 30 + assert b.m5(200) == b['m5'](200) == 1000 + assert b.g == b['g'] == ('getattr_hook', b, 'g') + assert False + Invocation from a class ----------------------- @@ -876,7 +954,7 @@ here is a pure Python equivalent: def x(self, value): self.__x = value @x.deleter - def delx(self): + def x(self): del self.__x ccc = CCC() From 2d8cd3ba915a8c686e7f51adfdaf69cf8ac03a72 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 19:07:21 -0800 Subject: [PATCH 29/32] Add tests for object_getattribute() --- Doc/howto/descriptor.rst | 101 +++++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 40 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 6ef01292299888..24cfb3826097a4 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -579,32 +579,15 @@ a pure Python equivalent: return cls_var # class variable raise AttributeError(name) -Interestingly, attribute lookup doesn't call :meth:`object.__getattribute__` -directly. Instead, both the dot operator and the :func:`getattr` function -perform attribute lookup by way of a helper function: - -.. testcode:: - - def getattr_hook(obj, name): - "Emulate slot_tp_getattr_hook() in Objects/typeobject.c" - try: - return obj.__getattribute__(name) - except AttributeError: - if not hasattr(type(obj), '__getattr__'): - raise - return type(obj).__getattr__(obj, name) # __getattr__ - -So if :meth:`__getattr__` exists, it is called whenever :meth:`__getattribute__` -raises :exc:`AttributeError` (either directly or in one of the descriptor calls). - -Also, if a user calls :meth:`object.__getattribute__` directly, the -:meth:`__getattr__` hook is bypassed entirely. .. testcode: :hide: + # Test the fidelity of object_getattribute() by comparing it with the + # normal object.__getattribute__(). The former will be accessed by + # square brackets and the latter by the dot operator. + class Object: - "Dotted access uses normal __getattribute__. Square brackets uses the emulation" def __getitem__(obj, name): try: @@ -614,7 +597,7 @@ Also, if a user calls :meth:`object.__getattribute__` directly, the raise return type(obj).__getattr__(obj, name) # __getattr__ - class A(Object): + class DualOperator(Object): x = 10 @@ -638,7 +621,7 @@ Also, if a user calls :meth:`object.__getattribute__` directly, the def __getattr__(self, name): return ('getattr_hook', self, name) - class B: + class DualOperatorWithSlots: __getitem__ = Object.__getitem__ @@ -660,23 +643,61 @@ Also, if a user calls :meth:`object.__getattribute__` directly, the return ('getattr_hook', self, name) - a = A(11) - vars(a).update(p3 = '_p3', m7 = '_m7') - assert a.x == a['x'] == 10 - assert a.z == a['z'] == 11 - assert a.p2 == a['p2'] == 20 - assert a.p3 == a['p3'] == 30 - assert a.m5(100) == a.m5(100) == 500 - assert a.m7 == a['m7'] == '_m7' - assert a.g == a['g'] == ('getattr_hook', a, 'g') - - b = B(22) - assert b.x == b['x'] == 15 - assert b.z == b['z'] == 22 - assert b.p2 == b['p2'] == 30 - assert b.m5(200) == b['m5'](200) == 1000 - assert b.g == b['g'] == ('getattr_hook', b, 'g') - assert False +.. doctest:: + :hide: + + >>> a = DualOperator(11) + >>> vars(a).update(p3 = '_p3', m7 = '_m7') + >>> a.x == a['x'] == 10 + True + >>> a.z == a['z'] == 11 + True + >>> a.p2 == a['p2'] == 20 + True + >>> a.p3 == a['p3'] == 30 + True + >>> a.m5(100) == a.m5(100) == 500 + True + >>> a.m7 == a['m7'] == '_m7' + True + >>> a.g == a['g'] == ('getattr_hook', a, 'g') + True + + >>> b = DualOperatorWithSlots(22) + True + >>> b.x == b['x'] == 15 + True + >>> b.z == b['z'] == 22 + True + >>> b.p2 == b['p2'] == 30 + True + >>> b.m5(200) == b['m5'](200) == 1000 + True + >>> b.g == b['g'] == ('getattr_hook', b, 'g') + True + >>> False + True + +Interestingly, attribute lookup doesn't call :meth:`object.__getattribute__` +directly. Instead, both the dot operator and the :func:`getattr` function +perform attribute lookup by way of a helper function: + +.. testcode:: + + def getattr_hook(obj, name): + "Emulate slot_tp_getattr_hook() in Objects/typeobject.c" + try: + return obj.__getattribute__(name) + except AttributeError: + if not hasattr(type(obj), '__getattr__'): + raise + return type(obj).__getattr__(obj, name) # __getattr__ + +So if :meth:`__getattr__` exists, it is called whenever :meth:`__getattribute__` +raises :exc:`AttributeError` (either directly or in one of the descriptor calls). + +Also, if a user calls :meth:`object.__getattribute__` directly, the +:meth:`__getattr__` hook is bypassed entirely. Invocation from a class From a623c65f242bd422a8a59b458717fb96871ee921 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 19:56:23 -0800 Subject: [PATCH 30/32] Convert assertion tests to regular doctests --- Doc/howto/descriptor.rst | 104 +++++++++++++++++++++++---------------- 1 file changed, 62 insertions(+), 42 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 24cfb3826097a4..497b890ddb6c1b 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -580,7 +580,7 @@ a pure Python equivalent: raise AttributeError(name) -.. testcode: +.. testcode:: :hide: # Test the fidelity of object_getattribute() by comparing it with the @@ -664,7 +664,6 @@ a pure Python equivalent: True >>> b = DualOperatorWithSlots(22) - True >>> b.x == b['x'] == 15 True >>> b.z == b['z'] == 22 @@ -675,8 +674,7 @@ a pure Python equivalent: True >>> b.g == b['g'] == ('getattr_hook', b, 'g') True - >>> False - True + Interestingly, attribute lookup doesn't call :meth:`object.__getattribute__` directly. Instead, both the dot operator and the :func:`getattr` function @@ -958,14 +956,7 @@ here is a pure Python equivalent: del self.__x x = Property(getx, setx, delx, "I'm the 'x' property.") - cc = CC() - assert not hasattr(cc, 'x') - cc.x = 33 - assert cc.x == 33 - del cc.x - assert not hasattr(cc, 'x') - - # Now do it again but the decorator style + # Now do it again but use the decorator style class CCC: @Property @@ -978,12 +969,29 @@ here is a pure Python equivalent: def x(self): del self.__x - ccc = CCC() - assert not hasattr(ccc, 'x') - ccc.x = 333 - assert ccc.x == 333 - del ccc.x - assert not hasattr(ccc, 'x') + +.. doctest:: + :hide: + + >>> cc = CC() + >>> hasattr(cc, 'x') + False + >>> cc.x = 33 + >>> cc.x + 33 + >>> del cc.x + >>> hasattr(cc, 'x') + False + + >>> ccc = CCC() + >>> hasattr(ccc, 'x') + False + >>> ccc.x = 333 + >>> ccc.x == 333 + True + >>> del ccc.x + >>> hasattr(ccc, 'x') + False The :func:`property` builtin helps whenever a user interface has granted attribute access and then subsequent changes require the intervention of a @@ -1240,18 +1248,24 @@ Using the non-data descriptor protocol, a pure Python version of return MethodType(self.f, cls) .. testcode:: - :hide: + :hide: - # Verify the emulation works - class T: - @ClassMethod - def cm(cls, x, y): - return (cls, x, y) - assert T.cm(11, 22) == (T, 11, 22) + # Verify the emulation works + class T: + @ClassMethod + def cm(cls, x, y): + return (cls, x, y) - # Also call it from an instance - t = T() - assert t.cm(11, 22) == (T, 11, 22) +.. doctest:: + :hide: + + >>> T.cm(11, 22) + (, 11, 22) + + # Also call it from an instance + >>> t = T() + >>> t.cm(11, 22) + (, 11, 22) The code path for ``hasattr(obj, '__get__')`` was added in Python 3.9 and makes it possible for :func:`classmethod` to support chained decorators. @@ -1475,13 +1489,15 @@ At this point, the metaclass has loaded member objects for *x* and *y*:: 'x': , 'y': } -.. testcode:: - :hide: +.. doctest:: + :hide: - # We test this separately because the preceding section is not - # doctestable due to the hex memory address for the __init__ function - assert isinstance(vars(H)['x'], Member) - assert isinstance(vars(H)['y'], Member) + # We test this separately because the preceding section is not + # doctestable due to the hex memory address for the __init__ function + >>> isinstance(vars(H)['x'], Member) + True + >>> isinstance(vars(H)['y'], Member) + True When instances are created, they have a ``slot_values`` list where the attributes are stored: @@ -1504,17 +1520,21 @@ Misspelled or unassigned attributes will raise an exception: ... AttributeError: 'H' object has no attribute 'xz' -.. testcode:: +.. doctest:: :hide: # Examples for deleted attributes are not shown because this section # is already a bit lengthy. But we still test that code here. - del h.x - assert not hasattr(h, 'x') + >>> del h.x + >>> hasattr(h, 'x') + False # Also test the code for uninitialized slots - class HU(Object, metaclass=Type): - slot_names = ['x', 'y'] - hu = HU() - assert not hasattr(hu, 'x') - assert not hasattr(hu, 'y') + >>> class HU(Object, metaclass=Type): + ... slot_names = ['x', 'y'] + ... + >>> hu = HU() + >>> hasattr(hu, 'x') + False + >>> hasattr(hu, 'y') + False From 26402344799f3f87200d4ea1104df59ded2005aa Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 20:19:55 -0800 Subject: [PATCH 31/32] Improve the example for classmethod with a property() --- Doc/howto/descriptor.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 497b890ddb6c1b..124fcf4954e9fe 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1281,7 +1281,7 @@ For example, a classmethod and property could be chained together: .. doctest:: - >>> G().__doc__ + >>> G.__doc__ "A doc for 'G'" @@ -1524,7 +1524,7 @@ Misspelled or unassigned attributes will raise an exception: :hide: # Examples for deleted attributes are not shown because this section - # is already a bit lengthy. But we still test that code here. + # is already a bit lengthy. We still test that code here. >>> del h.x >>> hasattr(h, 'x') False From b218c3d62fbe452ba744982a0a25b5d96bf6563b Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 24 Nov 2020 20:23:33 -0800 Subject: [PATCH 32/32] Add a blank line --- Doc/howto/descriptor.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 124fcf4954e9fe..e94f0ef88416ed 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -168,6 +168,7 @@ the lookup or update: def birthday(self): self.age += 1 # Calls both __get__() and __set__() + An interactive session shows that all access to the managed attribute *age* is logged, but that the regular attribute *name* is not logged::