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:
committed by
Damien George
parent
3a72f95919
commit
82db5c81e0
@@ -1,22 +1,28 @@
|
||||
class Descriptor:
|
||||
def __get__(self, obj, cls):
|
||||
print('get')
|
||||
print("get")
|
||||
print(type(obj) is Main)
|
||||
print(cls is Main)
|
||||
return 'result'
|
||||
return "result"
|
||||
|
||||
def __set__(self, obj, val):
|
||||
print('set')
|
||||
print("set")
|
||||
print(type(obj) is Main)
|
||||
print(val)
|
||||
|
||||
def __delete__(self, obj):
|
||||
print('delete')
|
||||
print("delete")
|
||||
print(type(obj) is Main)
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
print("set_name", name)
|
||||
print(owner.__name__ == "Main")
|
||||
|
||||
|
||||
class Main:
|
||||
Forward = Descriptor()
|
||||
|
||||
|
||||
m = Main()
|
||||
try:
|
||||
m.__class__
|
||||
@@ -26,15 +32,15 @@ except AttributeError:
|
||||
raise SystemExit
|
||||
|
||||
r = m.Forward
|
||||
if 'Descriptor' in repr(r.__class__):
|
||||
if "Descriptor" in repr(r.__class__):
|
||||
# Target doesn't support descriptors.
|
||||
print('SKIP')
|
||||
print("SKIP")
|
||||
raise SystemExit
|
||||
|
||||
# Test assignment and deletion.
|
||||
|
||||
print(r)
|
||||
m.Forward = 'a'
|
||||
m.Forward = "a"
|
||||
del m.Forward
|
||||
|
||||
# Test that lookup of descriptors like __get__ are not passed into __getattr__.
|
||||
|
||||
182
tests/basics/class_setname_hazard.py
Normal file
182
tests/basics/class_setname_hazard.py
Normal 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")
|
||||
111
tests/basics/class_setname_hazard_rand.py
Normal file
111
tests/basics/class_setname_hazard_rand.py
Normal 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))
|
||||
Reference in New Issue
Block a user