From 58b799dc66923796f68a7dca1b4992bfb3d66793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Wed, 17 Mar 2021 11:25:15 +0100 Subject: [PATCH 01/22] Fix wrong Sphinx markup preventing syntax highlighting --- 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 94aadd6f73a837..5c6f081bdaff8f 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1205,7 +1205,7 @@ example calls are unexciting: Using the non-data descriptor protocol, a pure Python version of :func:`staticmethod` would look like this: -.. doctest:: +.. testcode:: class StaticMethod: "Emulate PyStaticMethod_Type() in Objects/funcobject.c" From f77b0f9ad22230fb65565ff72670c24421ddf68f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Wed, 17 Mar 2021 11:32:31 +0100 Subject: [PATCH 02/22] Fix a bug in the ClassMethod Python equivalent --- 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 5c6f081bdaff8f..3e79512cf37e35 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1279,7 +1279,7 @@ Using the non-data descriptor protocol, a pure Python version of def __get__(self, obj, cls=None): if cls is None: cls = type(obj) - if hasattr(obj, '__get__'): + if hasattr(self.f, '__get__'): return self.f.__get__(cls) return MethodType(self.f, cls) From 1dbd876423420f06ec0517b5cf4bc186880ab09a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Wed, 17 Mar 2021 21:37:48 +0100 Subject: [PATCH 03/22] Fix the description of the ClassMethod Python equivalent --- 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 3e79512cf37e35..c3e8c8ba9daf07 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1303,7 +1303,7 @@ Using the non-data descriptor protocol, a pure Python version of >>> t.cm(11, 22) (, 11, 22) -The code path for ``hasattr(obj, '__get__')`` was added in Python 3.9 and +The code path for ``hasattr(self.f, '__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 d0d301eaf1ccb1b17533e90f430a6e0fbc06e0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Fri, 19 Mar 2021 11:50:24 +0100 Subject: [PATCH 04/22] Raise TypeError for __get__(None, None) calls --- Doc/howto/descriptor.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index c3e8c8ba9daf07..a66753d0e8c904 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -940,6 +940,8 @@ here is a pure Python equivalent: self._name = name def __get__(self, obj, objtype=None): + if obj is None and objtype is None: + raise TypeError('__get__(None, None) is invalid') if obj is None: return self if self.fget is None: @@ -1087,6 +1089,8 @@ during dotted lookup from an instance. Here's how it works: def __get__(self, obj, objtype=None): "Simulate func_descr_get() in Objects/funcobject.c" + if obj is None and objtype is None: + raise TypeError('__get__(None, None) is invalid') if obj is None: return self return MethodType(self, obj) @@ -1214,6 +1218,8 @@ Using the non-data descriptor protocol, a pure Python version of self.f = f def __get__(self, obj, objtype=None): + if obj is None and objtype is None: + raise TypeError('__get__(None, None) is invalid') return self.f @@ -1277,6 +1283,8 @@ Using the non-data descriptor protocol, a pure Python version of self.f = f def __get__(self, obj, cls=None): + if obj is None and cls is None: + raise TypeError('__get__(None, None) is invalid') if cls is None: cls = type(obj) if hasattr(self.f, '__get__'): @@ -1432,6 +1440,8 @@ by member descriptors: def __get__(self, obj, objtype=None): 'Emulate member_get() in Objects/descrobject.c' # Also see PyMember_GetOne() in Python/structmember.c + if obj is None and objtype is None: + raise TypeError('__get__(None, None) is invalid') value = obj._slotvalues[self.offset] if value is null: raise AttributeError(self.name) From e87fe373cf9c4fee636fd0dd261f6541947eb937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Mon, 22 Mar 2021 16:43:31 +0100 Subject: [PATCH 05/22] Fix a 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 a66753d0e8c904..3623d323e2c3e1 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -795,7 +795,7 @@ afterwards, :meth:`__set_name__` will need to be called manually. ORM example ----------- -The following code is simplified skeleton showing how data descriptors could +The following code is a simplified skeleton showing how data descriptors could be used to implement an `object relational mapping `_. From 52a06d03173752bb2053d46e2ff2e36b86b8e4f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Thu, 25 Mar 2021 21:23:03 +0100 Subject: [PATCH 06/22] Use the existing cls variable --- 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 3623d323e2c3e1..0f6092a6ffbade 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1500,7 +1500,7 @@ Python: cls = type(self) if hasattr(cls, 'slot_names') and name not in cls.slot_names: raise AttributeError( - f'{type(self).__name__!r} object has no attribute {name!r}' + f'{cls.__name__!r} object has no attribute {name!r}' ) super().__setattr__(name, value) @@ -1509,7 +1509,7 @@ Python: cls = type(self) if hasattr(cls, 'slot_names') and name not in cls.slot_names: raise AttributeError( - f'{type(self).__name__!r} object has no attribute {name!r}' + f'{cls.__name__!r} object has no attribute {name!r}' ) super().__delattr__(name) From 2f9971b1f59f2eff50eefcd23719461f05c95ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Wed, 31 Mar 2021 12:14:59 +0200 Subject: [PATCH 07/22] Allow keyword arguments in Object.__new__ --- 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 0f6092a6ffbade..f35fe40998cfee 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1487,7 +1487,7 @@ Python: class Object: 'Simulate how object.__new__() allocates memory for __slots__' - def __new__(cls, *args): + def __new__(cls, *args, **kwargs): 'Emulate object_new() in Objects/typeobject.c' inst = super().__new__(cls) if hasattr(cls, 'slot_names'): From 2f335ca69352169d3d5e8f2eeddf790024d29eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Wed, 31 Mar 2021 12:23:59 +0200 Subject: [PATCH 08/22] Use two more super() for consistency --- 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 f35fe40998cfee..bded6edad89258 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1476,7 +1476,7 @@ variables: slot_names = mapping.get('slot_names', []) for offset, name in enumerate(slot_names): mapping[name] = Member(name, clsname, offset) - return type.__new__(mcls, clsname, bases, mapping) + return super().__new__(mcls, clsname, bases, mapping) 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 @@ -1492,7 +1492,7 @@ Python: inst = super().__new__(cls) if hasattr(cls, 'slot_names'): empty_slots = [null] * len(cls.slot_names) - object.__setattr__(inst, '_slotvalues', empty_slots) + super().__setattr__(inst, '_slotvalues', empty_slots) return inst def __setattr__(self, name, value): From 4886dbbb4c6756a385cdc367d6dda9ebc131c2f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Wed, 31 Mar 2021 18:18:08 +0200 Subject: [PATCH 09/22] Add Object.__getattribute__ to raise AttributeError with the correct message for misspelled attributes (including __dict__) --- Doc/howto/descriptor.rst | 49 +++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index bded6edad89258..2dfacbe0b6c0dc 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1421,7 +1421,7 @@ 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 -``_slotvalues`` list. Reads and writes to that private structure are managed +``_slot_values`` list. Reads and writes to that private structure are managed by member descriptors: .. testcode:: @@ -1442,21 +1442,21 @@ by member descriptors: # Also see PyMember_GetOne() in Python/structmember.c if obj is None and objtype is None: raise TypeError('__get__(None, None) is invalid') - value = obj._slotvalues[self.offset] + value = obj._slot_values[self.offset] if value is null: raise AttributeError(self.name) return value def __set__(self, obj, value): 'Emulate member_set() in Objects/descrobject.c' - obj._slotvalues[self.offset] = value + obj._slot_values[self.offset] = value def __delete__(self, obj): 'Emulate member_delete() in Objects/descrobject.c' - value = obj._slotvalues[self.offset] + value = obj._slot_values[self.offset] if value is null: raise AttributeError(self.name) - obj._slotvalues[self.offset] = null + obj._slot_values[self.offset] = null def __repr__(self): 'Emulate member_repr() in Objects/descrobject.c' @@ -1492,9 +1492,19 @@ Python: inst = super().__new__(cls) if hasattr(cls, 'slot_names'): empty_slots = [null] * len(cls.slot_names) - super().__setattr__(inst, '_slotvalues', empty_slots) + super().__setattr__(inst, '_slot_values', empty_slots) return inst + def __getattribute__(self, name): + 'Emulate _PyObject_GenericGetAttrWithDict() Objects/object.c' + cls = type(self) + if (hasattr(cls, 'slot_names') and name not in cls.slot_names + and name != '_slot_values'): + raise AttributeError( + f'{cls.__name__!r} object has no attribute {name!r}' + ) + return super().__getattribute__(name) + def __setattr__(self, name, value): 'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c' cls = type(self) @@ -1548,26 +1558,39 @@ At this point, the metaclass has loaded member objects for *x* and *y*:: >>> isinstance(vars(H)['y'], Member) True -When instances are created, they have a ``slot_values`` list where the +When instances are created, they have a ``_slot_values`` list where the attributes are stored: .. doctest:: >>> h = H(10, 20) - >>> vars(h) - {'_slotvalues': [10, 20]} + >>> h._slot_values + [10, 20] >>> h.x = 55 - >>> vars(h) - {'_slotvalues': [55, 20]} + >>> h._slot_values + [55, 20] Misspelled or unassigned attributes will raise an exception: .. doctest:: - >>> h.xz + >>> vars(h) + Traceback (most recent call last): + ... + TypeError: vars() argument must have __dict__ attribute + >>> h.__dict__ + Traceback (most recent call last): + ... + AttributeError: 'H' object has no attribute '__dict__' + >>> h.z + Traceback (most recent call last): + ... + AttributeError: 'H' object has no attribute 'z' + >>> del h.y + >>> h.y Traceback (most recent call last): ... - AttributeError: 'H' object has no attribute 'xz' + AttributeError: 'y' .. doctest:: :hide: From 705c577a6d02c205cb2dd5232516a1364cca4b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Wed, 31 Mar 2021 20:37:44 +0200 Subject: [PATCH 10/22] Allow attribute lookup from a class in Member.__get__ --- Doc/howto/descriptor.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 2dfacbe0b6c0dc..553e1991aa2e8d 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1442,6 +1442,8 @@ by member descriptors: # Also see PyMember_GetOne() in Python/structmember.c if obj is None and objtype is None: raise TypeError('__get__(None, None) is invalid') + if obj is None: + return self value = obj._slot_values[self.offset] if value is null: raise AttributeError(self.name) @@ -1460,7 +1462,7 @@ by member descriptors: def __repr__(self): 'Emulate member_repr() in Objects/descrobject.c' - return f'' + return f'' The :meth:`type.__new__` method takes care of adding member objects to class variables: @@ -1553,9 +1555,9 @@ At this point, the metaclass has loaded member objects for *x* and *y*:: # 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) + >>> isinstance(H.x, Member) True - >>> isinstance(vars(H)['y'], Member) + >>> isinstance(H.y, Member) True When instances are created, they have a ``_slot_values`` list where the From c0e6432ca9d0f7af39a2f78bae70b5d6fe7a335d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Wed, 31 Mar 2021 22:43:20 +0200 Subject: [PATCH 11/22] Allow keyword arguments in Type.__new__ --- 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 553e1991aa2e8d..4d73022c54f246 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1472,7 +1472,7 @@ variables: class Type(type): 'Simulate how the type metaclass adds member objects for slots' - def __new__(mcls, clsname, bases, mapping): + def __new__(mcls, clsname, bases, mapping, **kwargs): 'Emuluate type_new() in Objects/typeobject.c' # type_new() calls PyTypeReady() which calls add_methods() slot_names = mapping.get('slot_names', []) From 81ec93c91a158f1aa6433219fb5c386694bba591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Thu, 1 Apr 2021 18:30:25 +0200 Subject: [PATCH 12/22] Raise ValueError for slot names conflicting with class variables --- Doc/howto/descriptor.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 4d73022c54f246..6bac06ff8a0b8d 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1476,7 +1476,11 @@ variables: 'Emuluate type_new() in Objects/typeobject.c' # type_new() calls PyTypeReady() which calls add_methods() slot_names = mapping.get('slot_names', []) - for offset, name in enumerate(slot_names): + for offset, name in enumerate(dict.fromkeys(slot_names)): + if name in namespace: + raise ValueError( + f'{name!r} in __slots__ conflicts with class variable' + ) mapping[name] = Member(name, clsname, offset) return super().__new__(mcls, clsname, bases, mapping) From 34eef3af642983cbf5d851ef80b5e76716dce676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Thu, 1 Apr 2021 19:47:53 +0200 Subject: [PATCH 13/22] Update descriptor.rst --- 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 6bac06ff8a0b8d..ea84de1030d5c1 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1477,7 +1477,7 @@ variables: # type_new() calls PyTypeReady() which calls add_methods() slot_names = mapping.get('slot_names', []) for offset, name in enumerate(dict.fromkeys(slot_names)): - if name in namespace: + if name in mapping: raise ValueError( f'{name!r} in __slots__ conflicts with class variable' ) From ae8c62267c5176b7b8572929d805bd6721d38f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Mon, 5 Apr 2021 16:47:34 +0200 Subject: [PATCH 14/22] Raise TypeError for non-empty __slots__ in subclasses of variable-length built-in types (int, tuple, bytes) --- Doc/howto/descriptor.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index ea84de1030d5c1..d0aae1ed0691e1 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1476,6 +1476,15 @@ variables: 'Emuluate type_new() in Objects/typeobject.c' # type_new() calls PyTypeReady() which calls add_methods() slot_names = mapping.get('slot_names', []) + if slot_names: + variable_length = {int, tuple, bytes} + for base in bases: + for cls in base.__mro__: + if cls in variable_length: + raise TypeError( + f'nonempty __slots__ not supported for ' + f'subtype of {cls.__name__!r}' + ) for offset, name in enumerate(dict.fromkeys(slot_names)): if name in mapping: raise ValueError( From 1d5fb9656848fcf823f4bf59b709c61d0f91f2f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Mon, 5 Apr 2021 19:32:10 +0200 Subject: [PATCH 15/22] Raise TypeError for non-empty __slots__ in subclasses of variable-length built-in types (int, tuple, bytes) --- Doc/howto/descriptor.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index d0aae1ed0691e1..5b22f1739966b3 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1477,10 +1477,9 @@ variables: # type_new() calls PyTypeReady() which calls add_methods() slot_names = mapping.get('slot_names', []) if slot_names: - variable_length = {int, tuple, bytes} for base in bases: for cls in base.__mro__: - if cls in variable_length: + if cls.__itemsize__: raise TypeError( f'nonempty __slots__ not supported for ' f'subtype of {cls.__name__!r}' From 4671b919391934699159ab5371010044d6e65e22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Tue, 10 May 2022 23:26:27 +0200 Subject: [PATCH 16/22] Apply requested changes and remove changes on the slots emulation --- Doc/howto/descriptor.rst | 93 +++++++++++----------------------------- 1 file changed, 24 insertions(+), 69 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 5b22f1739966b3..629ee848444d19 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -940,8 +940,6 @@ here is a pure Python equivalent: self._name = name def __get__(self, obj, objtype=None): - if obj is None and objtype is None: - raise TypeError('__get__(None, None) is invalid') if obj is None: return self if self.fget is None: @@ -1089,8 +1087,6 @@ during dotted lookup from an instance. Here's how it works: def __get__(self, obj, objtype=None): "Simulate func_descr_get() in Objects/funcobject.c" - if obj is None and objtype is None: - raise TypeError('__get__(None, None) is invalid') if obj is None: return self return MethodType(self, obj) @@ -1283,11 +1279,9 @@ Using the non-data descriptor protocol, a pure Python version of self.f = f def __get__(self, obj, cls=None): - if obj is None and cls is None: - raise TypeError('__get__(None, None) is invalid') if cls is None: cls = type(obj) - if hasattr(self.f, '__get__'): + if hasattr(type(self.f), '__get__'): return self.f.__get__(cls) return MethodType(self.f, cls) @@ -1311,7 +1305,7 @@ Using the non-data descriptor protocol, a pure Python version of >>> t.cm(11, 22) (, 11, 22) -The code path for ``hasattr(self.f, '__get__')`` was added in Python 3.9 and +The code path for ``hasattr(type(self.f), '__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: @@ -1381,7 +1375,7 @@ attributes stored in ``__slots__``: >>> mark.dept = 'Space Pirate' Traceback (most recent call last): ... - AttributeError: can't set attribute + AttributeError: property 'dept' of 'Immutable' object has no setter >>> mark.location = 'Mars' Traceback (most recent call last): ... @@ -1421,7 +1415,7 @@ 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 -``_slot_values`` list. Reads and writes to that private structure are managed +``_slotvalues`` list. Reads and writes to that private structure are managed by member descriptors: .. testcode:: @@ -1440,29 +1434,25 @@ by member descriptors: def __get__(self, obj, objtype=None): 'Emulate member_get() in Objects/descrobject.c' # Also see PyMember_GetOne() in Python/structmember.c - if obj is None and objtype is None: - raise TypeError('__get__(None, None) is invalid') - if obj is None: - return self - value = obj._slot_values[self.offset] + value = obj._slotvalues[self.offset] if value is null: raise AttributeError(self.name) return value def __set__(self, obj, value): 'Emulate member_set() in Objects/descrobject.c' - obj._slot_values[self.offset] = value + obj._slotvalues[self.offset] = value def __delete__(self, obj): 'Emulate member_delete() in Objects/descrobject.c' - value = obj._slot_values[self.offset] + value = obj._slotvalues[self.offset] if value is null: raise AttributeError(self.name) - obj._slot_values[self.offset] = null + obj._slotvalues[self.offset] = null def __repr__(self): 'Emulate member_repr() in Objects/descrobject.c' - return f'' + return f'' The :meth:`type.__new__` method takes care of adding member objects to class variables: @@ -1472,25 +1462,13 @@ variables: class Type(type): 'Simulate how the type metaclass adds member objects for slots' - def __new__(mcls, clsname, bases, mapping, **kwargs): - 'Emuluate type_new() in Objects/typeobject.c' + def __new__(mcls, clsname, bases, mapping): + 'Emulate type_new() in Objects/typeobject.c' # type_new() calls PyTypeReady() which calls add_methods() slot_names = mapping.get('slot_names', []) - if slot_names: - for base in bases: - for cls in base.__mro__: - if cls.__itemsize__: - raise TypeError( - f'nonempty __slots__ not supported for ' - f'subtype of {cls.__name__!r}' - ) - for offset, name in enumerate(dict.fromkeys(slot_names)): - if name in mapping: - raise ValueError( - f'{name!r} in __slots__ conflicts with class variable' - ) + for offset, name in enumerate(slot_names): mapping[name] = Member(name, clsname, offset) - return super().__new__(mcls, clsname, bases, mapping) + return type.__new__(mcls, clsname, bases, mapping) 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 @@ -1501,24 +1479,14 @@ Python: class Object: 'Simulate how object.__new__() allocates memory for __slots__' - def __new__(cls, *args, **kwargs): + def __new__(cls, *args): 'Emulate object_new() in Objects/typeobject.c' inst = super().__new__(cls) if hasattr(cls, 'slot_names'): empty_slots = [null] * len(cls.slot_names) - super().__setattr__(inst, '_slot_values', empty_slots) + object.__setattr__(inst, '_slotvalues', empty_slots) return inst - def __getattribute__(self, name): - 'Emulate _PyObject_GenericGetAttrWithDict() Objects/object.c' - cls = type(self) - if (hasattr(cls, 'slot_names') and name not in cls.slot_names - and name != '_slot_values'): - raise AttributeError( - f'{cls.__name__!r} object has no attribute {name!r}' - ) - return super().__getattribute__(name) - def __setattr__(self, name, value): 'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c' cls = type(self) @@ -1567,44 +1535,31 @@ At this point, the metaclass has loaded member objects for *x* and *y*:: # We test this separately because the preceding section is not # doctestable due to the hex memory address for the __init__ function - >>> isinstance(H.x, Member) + >>> isinstance(vars(H)['x'], Member) True - >>> isinstance(H.y, Member) + >>> isinstance(vars(H)['y'], Member) True -When instances are created, they have a ``_slot_values`` list where the +When instances are created, they have a ``slot_values`` list where the attributes are stored: .. doctest:: >>> h = H(10, 20) - >>> h._slot_values - [10, 20] + >>> vars(h) + {'_slotvalues': [10, 20]} >>> h.x = 55 - >>> h._slot_values - [55, 20] + >>> vars(h) + {'_slotvalues': [55, 20]} Misspelled or unassigned attributes will raise an exception: .. doctest:: - >>> vars(h) - Traceback (most recent call last): - ... - TypeError: vars() argument must have __dict__ attribute - >>> h.__dict__ - Traceback (most recent call last): - ... - AttributeError: 'H' object has no attribute '__dict__' - >>> h.z - Traceback (most recent call last): - ... - AttributeError: 'H' object has no attribute 'z' - >>> del h.y - >>> h.y + >>> h.xz Traceback (most recent call last): ... - AttributeError: 'y' + AttributeError: 'H' object has no attribute 'xz' .. doctest:: :hide: From 62fe4fcdfe141d1fa5edc1ec5fcacbcb8cf2a387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Tue, 10 May 2022 23:46:43 +0200 Subject: [PATCH 17/22] Update descriptor.rst --- Doc/howto/descriptor.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 98b209c842db2f..5d88221a3ef7a4 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1260,8 +1260,6 @@ Using the non-data descriptor protocol, a pure Python version of self.f = f def __get__(self, obj, objtype=None): - if obj is None and objtype is None: - raise TypeError('__get__(None, None) is invalid') return self.f def __call__(self, *args, **kwds): From 1252f4b0414e9ab39313be48ecbf456bf36f438e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Wed, 11 May 2022 00:55:55 +0200 Subject: [PATCH 18/22] Update descriptor.rst --- 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 5d88221a3ef7a4..fe8afd001d8eec 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1549,7 +1549,7 @@ variables: slot_names = mapping.get('slot_names', []) for offset, name in enumerate(slot_names): mapping[name] = Member(name, clsname, offset) - return type.__new__(mcls, clsname, bases, mapping) + return super().__new__(mcls, clsname, bases, mapping) 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 @@ -1565,7 +1565,7 @@ Python: inst = super().__new__(cls) if hasattr(cls, 'slot_names'): empty_slots = [null] * len(cls.slot_names) - object.__setattr__(inst, '_slotvalues', empty_slots) + super(Object, inst).__setattr__('_slotvalues', empty_slots) return inst def __setattr__(self, name, value): From 58ae977e18441c320bfb2de674a4b76ef3166c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Wed, 11 May 2022 01:09:06 +0200 Subject: [PATCH 19/22] Update descriptor.rst --- 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 fe8afd001d8eec..16ed41b2042cf0 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1543,7 +1543,7 @@ variables: class Type(type): 'Simulate how the type metaclass adds member objects for slots' - def __new__(mcls, clsname, bases, mapping): + def __new__(mcls, clsname, bases, mapping, **kwargs): 'Emulate type_new() in Objects/typeobject.c' # type_new() calls PyTypeReady() which calls add_methods() slot_names = mapping.get('slot_names', []) @@ -1560,7 +1560,7 @@ Python: class Object: 'Simulate how object.__new__() allocates memory for __slots__' - def __new__(cls, *args): + def __new__(cls, *args, **kwargs): 'Emulate object_new() in Objects/typeobject.c' inst = super().__new__(cls) if hasattr(cls, 'slot_names'): From 5630c64008f1edb7385f6421ea1800a21ccfb6d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Wed, 11 May 2022 01:16:51 +0200 Subject: [PATCH 20/22] Update descriptor.rst --- Doc/howto/descriptor.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 16ed41b2042cf0..9dcaa669edad41 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1515,6 +1515,8 @@ by member descriptors: def __get__(self, obj, objtype=None): 'Emulate member_get() in Objects/descrobject.c' # Also see PyMember_GetOne() in Python/structmember.c + if obj is None: + return self value = obj._slotvalues[self.offset] if value is null: raise AttributeError(self.name) From 5d45eaaee8ec9b59f32b785233e3d702b02b0fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Tue, 4 Oct 2022 23:26:00 +0200 Subject: [PATCH 21/22] Revert the two-argument form of super() and forward variadic arguments --- 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 9dcaa669edad41..e0e628c3e87251 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1551,7 +1551,7 @@ variables: slot_names = mapping.get('slot_names', []) for offset, name in enumerate(slot_names): mapping[name] = Member(name, clsname, offset) - return super().__new__(mcls, clsname, bases, mapping) + return type.__new__(mcls, clsname, bases, mapping, **kwargs) 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 @@ -1564,10 +1564,10 @@ Python: def __new__(cls, *args, **kwargs): 'Emulate object_new() in Objects/typeobject.c' - inst = super().__new__(cls) + inst = super().__new__(cls, *args, **kwargs) if hasattr(cls, 'slot_names'): empty_slots = [null] * len(cls.slot_names) - super(Object, inst).__setattr__('_slotvalues', empty_slots) + object.__setattr__(inst, '_slotvalues', empty_slots) return inst def __setattr__(self, name, value): From bcc9da0560045eaf8d8cd3cded33362694cbfc32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Ogam?= Date: Sun, 9 Oct 2022 00:09:42 +0200 Subject: [PATCH 22/22] Update descriptor.rst --- 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 2d42797ffa2bf6..74710d9b3fc2ed 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1584,7 +1584,7 @@ Python: def __new__(cls, *args, **kwargs): 'Emulate object_new() in Objects/typeobject.c' - inst = super().__new__(cls, *args, **kwargs) + inst = super().__new__(cls) if hasattr(cls, 'slot_names'): empty_slots = [null] * len(cls.slot_names) object.__setattr__(inst, '_slotvalues', empty_slots)