summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnson Mansfield <amansfield@mantaro.com>2024-07-19 13:01:09 -0400
committerDamien George <damien@micropython.org>2025-07-29 09:41:10 +1000
commit82db5c81e027f6ad305a43ec3c90a13ba319e3b4 (patch)
tree2f5ca384e0cecc78cfdacb39ec9ab1d054393c91
parent3a72f95919323d7a36cb3d153d92de90d64853a1 (diff)
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>
-rw-r--r--tests/basics/class_descriptor.py20
-rw-r--r--tests/basics/class_setname_hazard.py182
-rw-r--r--tests/basics/class_setname_hazard_rand.py111
3 files changed, 306 insertions, 7 deletions
diff --git a/tests/basics/class_descriptor.py b/tests/basics/class_descriptor.py
index 83d316743..feaed2fbb 100644
--- a/tests/basics/class_descriptor.py
+++ b/tests/basics/class_descriptor.py
@@ -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__.
diff --git a/tests/basics/class_setname_hazard.py b/tests/basics/class_setname_hazard.py
new file mode 100644
index 000000000..77c040934
--- /dev/null
+++ b/tests/basics/class_setname_hazard.py
@@ -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")
diff --git a/tests/basics/class_setname_hazard_rand.py b/tests/basics/class_setname_hazard_rand.py
new file mode 100644
index 000000000..4c9934c3b
--- /dev/null
+++ b/tests/basics/class_setname_hazard_rand.py
@@ -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))