tests/basics: Add tests for PEP487 __set_name__.

Including the stochastic tests needed to guarantee sensitivity to the
potential iterate-while-modifying hazard a naive implementation might have.

Signed-off-by: Anson Mansfield <amansfield@mantaro.com>
This commit is contained in:
Anson Mansfield
2024-07-19 13:01:09 -04:00
committed by Damien George
parent 3a72f95919
commit 82db5c81e0
3 changed files with 306 additions and 7 deletions

View File

@@ -1,22 +1,28 @@
class Descriptor: class Descriptor:
def __get__(self, obj, cls): def __get__(self, obj, cls):
print('get') print("get")
print(type(obj) is Main) print(type(obj) is Main)
print(cls is Main) print(cls is Main)
return 'result' return "result"
def __set__(self, obj, val): def __set__(self, obj, val):
print('set') print("set")
print(type(obj) is Main) print(type(obj) is Main)
print(val) print(val)
def __delete__(self, obj): def __delete__(self, obj):
print('delete') print("delete")
print(type(obj) is Main) print(type(obj) is Main)
def __set_name__(self, owner, name):
print("set_name", name)
print(owner.__name__ == "Main")
class Main: class Main:
Forward = Descriptor() Forward = Descriptor()
m = Main() m = Main()
try: try:
m.__class__ m.__class__
@@ -26,15 +32,15 @@ except AttributeError:
raise SystemExit raise SystemExit
r = m.Forward r = m.Forward
if 'Descriptor' in repr(r.__class__): if "Descriptor" in repr(r.__class__):
# Target doesn't support descriptors. # Target doesn't support descriptors.
print('SKIP') print("SKIP")
raise SystemExit raise SystemExit
# Test assignment and deletion. # Test assignment and deletion.
print(r) print(r)
m.Forward = 'a' m.Forward = "a"
del m.Forward del m.Forward
# Test that lookup of descriptors like __get__ are not passed into __getattr__. # Test that lookup of descriptors like __get__ are not passed into __getattr__.

View File

@@ -0,0 +1,182 @@
# Test that __set_name__ can access and mutate its owner argument.
def skip_if_no_descriptors():
class Descriptor:
def __get__(self, obj, cls):
return
class TestClass:
Forward = Descriptor()
a = TestClass()
try:
a.__class__
except AttributeError:
# Target doesn't support __class__.
print("SKIP")
raise SystemExit
b = a.Forward
if "Descriptor" in repr(b.__class__):
# Target doesn't support descriptors.
print("SKIP")
raise SystemExit
skip_if_no_descriptors()
# Test basic accesses and mutations.
class GetSibling:
def __set_name__(self, owner, name):
print(getattr(owner, name + "_sib"))
class GetSiblingTest:
desc = GetSibling()
desc_sib = 111
t110 = GetSiblingTest()
class SetSibling:
def __set_name__(self, owner, name):
setattr(owner, name + "_sib", 121)
class SetSiblingTest:
desc = SetSibling()
t120 = SetSiblingTest()
print(t120.desc_sib)
class DelSibling:
def __set_name__(self, owner, name):
delattr(owner, name + "_sib")
class DelSiblingTest:
desc = DelSibling()
desc_sib = 131
t130 = DelSiblingTest()
try:
print(t130.desc_sib)
except AttributeError:
print("AttributeError")
class GetSelf:
x = 211
def __set_name__(self, owner, name):
print(getattr(owner, name).x)
class GetSelfTest:
desc = GetSelf()
t210 = GetSelfTest()
class SetSelf:
def __set_name__(self, owner, name):
setattr(owner, name, 221)
class SetSelfTest:
desc = SetSelf()
t220 = SetSelfTest()
print(t220.desc)
class DelSelf:
def __set_name__(self, owner, name):
delattr(owner, name)
class DelSelfTest:
desc = DelSelf()
t230 = DelSelfTest()
try:
print(t230.desc)
except AttributeError:
print("AttributeError")
# Test exception behavior.
class Raise:
def __set_name__(self, owner, name):
raise Exception()
try:
class RaiseTest:
desc = Raise()
except Exception as e: # CPython raises RuntimeError, MicroPython propagates the original exception
print("Exception")
# Ensure removed/overwritten class members still get __set_name__ called.
class SetSpecific:
def __init__(self, sib_name, sib_replace):
self.sib_name = sib_name
self.sib_replace = sib_replace
def __set_name__(self, owner, name):
setattr(owner, self.sib_name, self.sib_replace)
class SetReplaceTest:
a = SetSpecific("b", 312) # one of these is changed first
b = SetSpecific("a", 311)
t310 = SetReplaceTest()
print(t310.a)
print(t310.b)
class DelSpecific:
def __init__(self, sib_name):
self.sib_name = sib_name
def __set_name__(self, owner, name):
delattr(owner, self.sib_name)
class DelReplaceTest:
a = DelSpecific("b") # one of these is removed first
b = DelSpecific("a")
t320 = DelReplaceTest()
try:
print(t320.a)
except AttributeError:
print("AttributeError")
try:
print(t320.b)
except AttributeError:
print("AttributeError")

View File

@@ -0,0 +1,111 @@
# Test to make sure there's no sequence hazard even when a __set_name__ implementation
# mutates and reorders the namespace of its owner class.
# VERY hard bug to prove out except via a stochastic test.
try:
from random import choice
import re
except ImportError:
print("SKIP")
raise SystemExit
def skip_if_no_descriptors():
class Descriptor:
def __get__(self, obj, cls):
return
class TestClass:
Forward = Descriptor()
a = TestClass()
try:
a.__class__
except AttributeError:
# Target doesn't support __class__.
print("SKIP")
raise SystemExit
b = a.Forward
if "Descriptor" in repr(b.__class__):
# Target doesn't support descriptors.
print("SKIP")
raise SystemExit
skip_if_no_descriptors()
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
# Would be r"[A-Z]{5}", but not all ports support the {n} quantifier.
junk_re = re.compile(r"[A-Z][A-Z][A-Z][A-Z][A-Z]")
def junk_fill(obj, n=10): # Add randomly-generated attributes to an object.
for i in range(n):
name = "".join(choice(letters) for j in range(5))
setattr(obj, name, object())
def junk_clear(obj): # Remove attributes added by junk_fill.
to_del = [name for name in dir(obj) if junk_re.match(name)]
for name in to_del:
delattr(obj, name)
def junk_sequencer():
global runs
try:
while True:
owner, name = yield
runs[name] = runs.get(name, 0) + 1
junk_fill(owner)
finally:
junk_clear(owner)
class JunkMaker:
def __set_name__(self, owner, name):
global seq
seq.send((owner, name))
runs = {}
seq = junk_sequencer()
next(seq)
class Main:
a = JunkMaker()
b = JunkMaker()
c = JunkMaker()
d = JunkMaker()
e = JunkMaker()
f = JunkMaker()
g = JunkMaker()
h = JunkMaker()
i = JunkMaker()
j = JunkMaker()
k = JunkMaker()
l = JunkMaker()
m = JunkMaker()
n = JunkMaker()
o = JunkMaker()
p = JunkMaker()
q = JunkMaker()
r = JunkMaker()
s = JunkMaker()
t = JunkMaker()
u = JunkMaker()
v = JunkMaker()
w = JunkMaker()
x = JunkMaker()
y = JunkMaker()
z = JunkMaker()
seq.close()
for k in letters.lower():
print(k, runs.get(k, 0))