Skip to content

Commit 82db5c8

Browse files
AJMansfielddpgeorge
authored andcommitted
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 <[email protected]>
1 parent 3a72f95 commit 82db5c8

File tree

3 files changed

+306
-7
lines changed

3 files changed

+306
-7
lines changed

tests/basics/class_descriptor.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
class Descriptor:
22
def __get__(self, obj, cls):
3-
print('get')
3+
print("get")
44
print(type(obj) is Main)
55
print(cls is Main)
6-
return 'result'
6+
return "result"
77

88
def __set__(self, obj, val):
9-
print('set')
9+
print("set")
1010
print(type(obj) is Main)
1111
print(val)
1212

1313
def __delete__(self, obj):
14-
print('delete')
14+
print("delete")
1515
print(type(obj) is Main)
1616

17+
def __set_name__(self, owner, name):
18+
print("set_name", name)
19+
print(owner.__name__ == "Main")
20+
21+
1722
class Main:
1823
Forward = Descriptor()
1924

25+
2026
m = Main()
2127
try:
2228
m.__class__
@@ -26,15 +32,15 @@ class Main:
2632
raise SystemExit
2733

2834
r = m.Forward
29-
if 'Descriptor' in repr(r.__class__):
35+
if "Descriptor" in repr(r.__class__):
3036
# Target doesn't support descriptors.
31-
print('SKIP')
37+
print("SKIP")
3238
raise SystemExit
3339

3440
# Test assignment and deletion.
3541

3642
print(r)
37-
m.Forward = 'a'
43+
m.Forward = "a"
3844
del m.Forward
3945

4046
# Test that lookup of descriptors like __get__ are not passed into __getattr__.

tests/basics/class_setname_hazard.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Test that __set_name__ can access and mutate its owner argument.
2+
3+
4+
def skip_if_no_descriptors():
5+
class Descriptor:
6+
def __get__(self, obj, cls):
7+
return
8+
9+
class TestClass:
10+
Forward = Descriptor()
11+
12+
a = TestClass()
13+
try:
14+
a.__class__
15+
except AttributeError:
16+
# Target doesn't support __class__.
17+
print("SKIP")
18+
raise SystemExit
19+
20+
b = a.Forward
21+
if "Descriptor" in repr(b.__class__):
22+
# Target doesn't support descriptors.
23+
print("SKIP")
24+
raise SystemExit
25+
26+
27+
skip_if_no_descriptors()
28+
29+
30+
# Test basic accesses and mutations.
31+
32+
33+
class GetSibling:
34+
def __set_name__(self, owner, name):
35+
print(getattr(owner, name + "_sib"))
36+
37+
38+
class GetSiblingTest:
39+
desc = GetSibling()
40+
desc_sib = 111
41+
42+
43+
t110 = GetSiblingTest()
44+
45+
46+
class SetSibling:
47+
def __set_name__(self, owner, name):
48+
setattr(owner, name + "_sib", 121)
49+
50+
51+
class SetSiblingTest:
52+
desc = SetSibling()
53+
54+
55+
t120 = SetSiblingTest()
56+
57+
print(t120.desc_sib)
58+
59+
60+
class DelSibling:
61+
def __set_name__(self, owner, name):
62+
delattr(owner, name + "_sib")
63+
64+
65+
class DelSiblingTest:
66+
desc = DelSibling()
67+
desc_sib = 131
68+
69+
70+
t130 = DelSiblingTest()
71+
72+
try:
73+
print(t130.desc_sib)
74+
except AttributeError:
75+
print("AttributeError")
76+
77+
78+
class GetSelf:
79+
x = 211
80+
81+
def __set_name__(self, owner, name):
82+
print(getattr(owner, name).x)
83+
84+
85+
class GetSelfTest:
86+
desc = GetSelf()
87+
88+
89+
t210 = GetSelfTest()
90+
91+
92+
class SetSelf:
93+
def __set_name__(self, owner, name):
94+
setattr(owner, name, 221)
95+
96+
97+
class SetSelfTest:
98+
desc = SetSelf()
99+
100+
101+
t220 = SetSelfTest()
102+
103+
print(t220.desc)
104+
105+
106+
class DelSelf:
107+
def __set_name__(self, owner, name):
108+
delattr(owner, name)
109+
110+
111+
class DelSelfTest:
112+
desc = DelSelf()
113+
114+
115+
t230 = DelSelfTest()
116+
117+
try:
118+
print(t230.desc)
119+
except AttributeError:
120+
print("AttributeError")
121+
122+
123+
# Test exception behavior.
124+
125+
126+
class Raise:
127+
def __set_name__(self, owner, name):
128+
raise Exception()
129+
130+
131+
try:
132+
133+
class RaiseTest:
134+
desc = Raise()
135+
except Exception as e: # CPython raises RuntimeError, MicroPython propagates the original exception
136+
print("Exception")
137+
138+
139+
# Ensure removed/overwritten class members still get __set_name__ called.
140+
141+
142+
class SetSpecific:
143+
def __init__(self, sib_name, sib_replace):
144+
self.sib_name = sib_name
145+
self.sib_replace = sib_replace
146+
147+
def __set_name__(self, owner, name):
148+
setattr(owner, self.sib_name, self.sib_replace)
149+
150+
151+
class SetReplaceTest:
152+
a = SetSpecific("b", 312) # one of these is changed first
153+
b = SetSpecific("a", 311)
154+
155+
156+
t310 = SetReplaceTest()
157+
print(t310.a)
158+
print(t310.b)
159+
160+
161+
class DelSpecific:
162+
def __init__(self, sib_name):
163+
self.sib_name = sib_name
164+
165+
def __set_name__(self, owner, name):
166+
delattr(owner, self.sib_name)
167+
168+
169+
class DelReplaceTest:
170+
a = DelSpecific("b") # one of these is removed first
171+
b = DelSpecific("a")
172+
173+
174+
t320 = DelReplaceTest()
175+
try:
176+
print(t320.a)
177+
except AttributeError:
178+
print("AttributeError")
179+
try:
180+
print(t320.b)
181+
except AttributeError:
182+
print("AttributeError")
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Test to make sure there's no sequence hazard even when a __set_name__ implementation
2+
# mutates and reorders the namespace of its owner class.
3+
# VERY hard bug to prove out except via a stochastic test.
4+
5+
6+
try:
7+
from random import choice
8+
import re
9+
except ImportError:
10+
print("SKIP")
11+
raise SystemExit
12+
13+
14+
def skip_if_no_descriptors():
15+
class Descriptor:
16+
def __get__(self, obj, cls):
17+
return
18+
19+
class TestClass:
20+
Forward = Descriptor()
21+
22+
a = TestClass()
23+
try:
24+
a.__class__
25+
except AttributeError:
26+
# Target doesn't support __class__.
27+
print("SKIP")
28+
raise SystemExit
29+
30+
b = a.Forward
31+
if "Descriptor" in repr(b.__class__):
32+
# Target doesn't support descriptors.
33+
print("SKIP")
34+
raise SystemExit
35+
36+
37+
skip_if_no_descriptors()
38+
39+
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
40+
41+
# Would be r"[A-Z]{5}", but not all ports support the {n} quantifier.
42+
junk_re = re.compile(r"[A-Z][A-Z][A-Z][A-Z][A-Z]")
43+
44+
45+
def junk_fill(obj, n=10): # Add randomly-generated attributes to an object.
46+
for i in range(n):
47+
name = "".join(choice(letters) for j in range(5))
48+
setattr(obj, name, object())
49+
50+
51+
def junk_clear(obj): # Remove attributes added by junk_fill.
52+
to_del = [name for name in dir(obj) if junk_re.match(name)]
53+
for name in to_del:
54+
delattr(obj, name)
55+
56+
57+
def junk_sequencer():
58+
global runs
59+
try:
60+
while True:
61+
owner, name = yield
62+
runs[name] = runs.get(name, 0) + 1
63+
junk_fill(owner)
64+
finally:
65+
junk_clear(owner)
66+
67+
68+
class JunkMaker:
69+
def __set_name__(self, owner, name):
70+
global seq
71+
seq.send((owner, name))
72+
73+
74+
runs = {}
75+
seq = junk_sequencer()
76+
next(seq)
77+
78+
79+
class Main:
80+
a = JunkMaker()
81+
b = JunkMaker()
82+
c = JunkMaker()
83+
d = JunkMaker()
84+
e = JunkMaker()
85+
f = JunkMaker()
86+
g = JunkMaker()
87+
h = JunkMaker()
88+
i = JunkMaker()
89+
j = JunkMaker()
90+
k = JunkMaker()
91+
l = JunkMaker()
92+
m = JunkMaker()
93+
n = JunkMaker()
94+
o = JunkMaker()
95+
p = JunkMaker()
96+
q = JunkMaker()
97+
r = JunkMaker()
98+
s = JunkMaker()
99+
t = JunkMaker()
100+
u = JunkMaker()
101+
v = JunkMaker()
102+
w = JunkMaker()
103+
x = JunkMaker()
104+
y = JunkMaker()
105+
z = JunkMaker()
106+
107+
108+
seq.close()
109+
110+
for k in letters.lower():
111+
print(k, runs.get(k, 0))

0 commit comments

Comments
 (0)