mirror of
https://github.com/RustPython/RustPython.git
synced 2026-06-02 19:39:49 +09:00
* Emit TO_BOOL before conditional jumps, fix class/module prologue - Emit TO_BOOL before POP_JUMP_IF_TRUE/FALSE in the general case of compile_jump_if (Compare expressions excluded since they already produce a bool) - Module-level __doc__: use STORE_NAME instead of STORE_GLOBAL - Class body __module__: use LOAD_NAME instead of LOAD_GLOBAL - Class body: store __firstlineno__ before __doc__ * Emit MAKE_CELL and COPY_FREE_VARS before RESUME Emit MAKE_CELL for each cell variable and COPY_FREE_VARS N for free variables at the start of each code object, before RESUME. These instructions are no-ops in the VM but align the bytecode with CPython 3.14's output. * Emit __static_attributes__ at end of class bodies Store a tuple of attribute names (currently always empty) as __static_attributes__ in the class namespace, matching CPython 3.14's class body epilogue. Attribute name collection from self.xxx accesses is a follow-up task. * Remove expectedFailure from DictProxyTests iter tests test_iter_keys, test_iter_values, test_iter_items now pass because class bodies emit __static_attributes__ and __firstlineno__, matching the expected dict key set. * Use 1-based stack indexing for LIST_EXTEND, SET_UPDATE, etc. Switch LIST_APPEND, LIST_EXTEND, SET_ADD, SET_UPDATE, MAP_ADD from 0-based to 1-based stack depth argument, matching CPython's PEEK(oparg) convention. Adjust the VM to subtract 1 before calling nth_value. * Use plain LOAD_ATTR + PUSH_NULL for calls on imported names When the call target is an attribute of an imported name (e.g., logging.getLogger()), use plain LOAD_ATTR (method_flag=0) with a separate PUSH_NULL instead of method-mode LOAD_ATTR. This matches CPython 3.14's behavior which avoids the method call optimization for module attribute access. * Duplicate return-None epilogue for fall-through blocks When the last block in a code object is exactly LOAD_CONST None + RETURN_VALUE (the implicit return), duplicate these instructions into blocks that would otherwise fall through to it. This matches CPython 3.14's behavior of giving each code path its own explicit return instruction. * Run cargo fmt on ir.rs * Remove expectedFailure from test_intrinsic_1 in test_dis * Emit TO_BOOL before conditional jumps for all expressions including Compare * Add __classdict__ cell for classes with function definitions Set needs_classdict=true for class scopes that contain function definitions (def/async def), matching CPython 3.14's behavior for PEP 649 deferred annotation support. Also restore the Compare expression check in compile_jump_if to skip TO_BOOL for comparison operations. * Emit __classdictcell__ store in class body epilogue Store the __classdict__ cell reference as __classdictcell__ in the class namespace when the class has __classdict__ as a cell variable. Uses LOAD_DEREF (RustPython separates cell vars from fast locals unlike CPython's unified array). * Always run DCE to remove dead code after terminal instructions Run basic dead code elimination (truncating instructions after RETURN_VALUE/RAISE/JUMP within blocks) at all optimization levels, not just optimize > 0. CPython always removes this dead code during assembly. * Restrict LOAD_ATTR plain mode to module/class scope imports Only use plain LOAD_ATTR + PUSH_NULL for imports at module or class scope. Function-local imports use method call mode LOAD_ATTR, matching CPython 3.14's behavior. * Eliminate unreachable blocks after jump normalization Split DCE into two phases: (1) within-block truncation after terminal instructions (always runs), (2) whole-block elimination for blocks only reachable via fall-through from terminal blocks (runs after normalize_jumps when dead jump instructions exist). * Fold BUILD_TUPLE 0 into LOAD_CONST empty tuple Convert BUILD_TUPLE with size 0 to LOAD_CONST () during constant folding, matching CPython's optimization for empty tuple literals. * Handle __classcell__ and __classdictcell__ in type.__new__ - Remove __classcell__ from class dict after setting the cell value - Add __classdictcell__ handling: set cell to class namespace dict, then remove from class dict - Register __classdictcell__ identifier - Use LoadClosure instead of LoadDeref for __classdictcell__ emission - Reorder MakeFunctionFlag bits to match CPython - Run ruff format on scripts * Revert __classdict__ cell and __classdictcell__ changes The __classdict__ cell addition (for classes with function defs) and __classdictcell__ store caused cell initialization failures in importlib. These require deeper VM changes to properly support the cell variable lifecycle. Reverted for stability. * Fix unreachable block elimination with fixpoint reachability Use fixpoint iteration to properly determine block reachability: only mark jump targets of already-reachable blocks, preventing orphaned blocks from falsely marking their targets as reachable. Also add a final DCE pass after assembly NOP removal to catch dead code created by normalize_jumps. * Check enclosing scopes for IMPORTED flag in LOAD_ATTR mode When deciding whether to use plain LOAD_ATTR for attribute calls, check if the name is imported in any enclosing scope (not just the current scope). This handles the common pattern where a module is imported at module level but used inside functions. * Add __classdict__ cell for classes with function definitions Set needs_classdict=true when a class scope contains function definitions (def/async def), matching CPython 3.14 which always creates a __classdict__ cell for PEP 649 support in such classes. * Store __classdictcell__ in class body epilogue Store the __classdict__ cell reference as __classdictcell__ in the class namespace using LoadClosure (which loads the cell object itself, not the value inside). This matches CPython 3.14's class body epilogue. * Fix clippy collapsible_if warnings and cargo fmt * Revert __classdict__ and __classdictcell__ changes (cause import failures) * Revert type.__new__ __classcell__ removal and __classdictcell__ handling Revert the class cell cleanup changes from e6975f973 that cause import failures when frozen module bytecode is stale. The original behavior (not removing __classcell__ from class dict) is restored. * Re-add __classdict__ cell and __classdictcell__ store Restore the __classdict__ cell for classes with function definitions and __classdictcell__ store in class body epilogue. Previous failure was caused by stale .pyc cache files containing bytecode from an intermediate MakeFunctionFlag reorder attempt, not by these changes themselves. * Reorder MakeFunctionFlag to match CPython's SET_FUNCTION_ATTRIBUTE Reorder discriminants: Defaults=0, KwOnlyDefaults=1, Annotations=2, Closure=3, Annotate=4, TypeParams=5. This aligns the oparg values with CPython 3.14's convention. Note: after this change, stale .pyc cache files must be deleted (find . -name '*.pyc' -delete) to avoid bytecode mismatch errors. * Use CPython-compatible power-of-two encoding for SET_FUNCTION_ATTRIBUTE Override From/TryFrom for MakeFunctionFlag to use power-of-two values (1,2,4,8,16,32) matching CPython's SET_FUNCTION_ATTRIBUTE oparg encoding, instead of sequential discriminants (0,1,2,3,4,5). * Remove expectedFailure from test_elim_jump_after_return1 and test_no_jump_over_return_out_of_finally_block * Remove __classcell__ and __classdictcell__ from class dict in type.__new__ * Remove expectedFailure from test___classcell___expected_behaviour, cargo fmt * Handle MakeCell and CopyFreeVars as no-ops in JIT These prologue instructions are handled at frame creation time by the VM. The JIT operates on already-initialized frames, so these can be safely skipped during compilation. * Remove expectedFailure from test_load_fast_known_simple * Restore expectedFailure for test_load_fast_known_simple The test expects LOAD_FAST_BORROW_LOAD_FAST_BORROW superinstruction which RustPython does not emit yet.
550 lines
16 KiB
Python
Vendored
550 lines
16 KiB
Python
Vendored
"""Unit tests for zero-argument super() & related machinery."""
|
|
|
|
import textwrap
|
|
import threading
|
|
import unittest
|
|
from unittest.mock import patch
|
|
from test.support import import_helper, threading_helper
|
|
|
|
|
|
ADAPTIVE_WARMUP_DELAY = 2
|
|
|
|
|
|
class A:
|
|
def f(self):
|
|
return 'A'
|
|
@classmethod
|
|
def cm(cls):
|
|
return (cls, 'A')
|
|
|
|
class B(A):
|
|
def f(self):
|
|
return super().f() + 'B'
|
|
@classmethod
|
|
def cm(cls):
|
|
return (cls, super().cm(), 'B')
|
|
|
|
class C(A):
|
|
def f(self):
|
|
return super().f() + 'C'
|
|
@classmethod
|
|
def cm(cls):
|
|
return (cls, super().cm(), 'C')
|
|
|
|
class D(C, B):
|
|
def f(self):
|
|
return super().f() + 'D'
|
|
def cm(cls):
|
|
return (cls, super().cm(), 'D')
|
|
|
|
class E(D):
|
|
pass
|
|
|
|
class F(E):
|
|
f = E.f
|
|
|
|
class G(A):
|
|
pass
|
|
|
|
|
|
class TestSuper(unittest.TestCase):
|
|
|
|
def tearDown(self):
|
|
# This fixes the damage that test_various___class___pathologies does.
|
|
nonlocal __class__
|
|
__class__ = TestSuper
|
|
|
|
def test_basics_working(self):
|
|
self.assertEqual(D().f(), 'ABCD')
|
|
|
|
def test_class_getattr_working(self):
|
|
self.assertEqual(D.f(D()), 'ABCD')
|
|
|
|
def test_subclass_no_override_working(self):
|
|
self.assertEqual(E().f(), 'ABCD')
|
|
self.assertEqual(E.f(E()), 'ABCD')
|
|
|
|
def test_unbound_method_transfer_working(self):
|
|
self.assertEqual(F().f(), 'ABCD')
|
|
self.assertEqual(F.f(F()), 'ABCD')
|
|
|
|
def test_class_methods_still_working(self):
|
|
self.assertEqual(A.cm(), (A, 'A'))
|
|
self.assertEqual(A().cm(), (A, 'A'))
|
|
self.assertEqual(G.cm(), (G, 'A'))
|
|
self.assertEqual(G().cm(), (G, 'A'))
|
|
|
|
def test_super_in_class_methods_working(self):
|
|
d = D()
|
|
self.assertEqual(d.cm(), (d, (D, (D, (D, 'A'), 'B'), 'C'), 'D'))
|
|
e = E()
|
|
self.assertEqual(e.cm(), (e, (E, (E, (E, 'A'), 'B'), 'C'), 'D'))
|
|
|
|
def test_super_with_closure(self):
|
|
# Issue4360: super() did not work in a function that
|
|
# contains a closure
|
|
class E(A):
|
|
def f(self):
|
|
def nested():
|
|
self
|
|
return super().f() + 'E'
|
|
|
|
self.assertEqual(E().f(), 'AE')
|
|
|
|
# TODO: RUSTPYTHON; SyntaxError: name '__class__' is assigned to before global declaration
|
|
'''
|
|
def test_various___class___pathologies(self):
|
|
# See issue #12370
|
|
class X(A):
|
|
def f(self):
|
|
return super().f()
|
|
__class__ = 413
|
|
x = X()
|
|
self.assertEqual(x.f(), 'A')
|
|
self.assertEqual(x.__class__, 413)
|
|
class X:
|
|
x = __class__
|
|
def f():
|
|
__class__
|
|
self.assertIs(X.x, type(self))
|
|
with self.assertRaises(NameError) as e:
|
|
exec("""class X:
|
|
__class__
|
|
def f():
|
|
__class__""", globals(), {})
|
|
self.assertIs(type(e.exception), NameError) # Not UnboundLocalError
|
|
class X:
|
|
global __class__
|
|
__class__ = 42
|
|
def f():
|
|
__class__
|
|
self.assertEqual(globals()["__class__"], 42)
|
|
del globals()["__class__"]
|
|
self.assertNotIn("__class__", X.__dict__)
|
|
class X:
|
|
nonlocal __class__
|
|
__class__ = 42
|
|
def f():
|
|
__class__
|
|
self.assertEqual(__class__, 42)
|
|
'''
|
|
|
|
def test___class___instancemethod(self):
|
|
# See issue #14857
|
|
class X:
|
|
def f(self):
|
|
return __class__
|
|
self.assertIs(X().f(), X)
|
|
|
|
def test___class___classmethod(self):
|
|
# See issue #14857
|
|
class X:
|
|
@classmethod
|
|
def f(cls):
|
|
return __class__
|
|
self.assertIs(X.f(), X)
|
|
|
|
def test___class___staticmethod(self):
|
|
# See issue #14857
|
|
class X:
|
|
@staticmethod
|
|
def f():
|
|
return __class__
|
|
self.assertIs(X.f(), X)
|
|
|
|
def test___class___new(self):
|
|
# See issue #23722
|
|
# Ensure zero-arg super() works as soon as type.__new__() is completed
|
|
test_class = None
|
|
|
|
class Meta(type):
|
|
def __new__(cls, name, bases, namespace):
|
|
nonlocal test_class
|
|
self = super().__new__(cls, name, bases, namespace)
|
|
test_class = self.f()
|
|
return self
|
|
|
|
class A(metaclass=Meta):
|
|
@staticmethod
|
|
def f():
|
|
return __class__
|
|
|
|
self.assertIs(test_class, A)
|
|
|
|
def test___class___delayed(self):
|
|
# See issue #23722
|
|
test_namespace = None
|
|
|
|
class Meta(type):
|
|
def __new__(cls, name, bases, namespace):
|
|
nonlocal test_namespace
|
|
test_namespace = namespace
|
|
return None
|
|
|
|
class A(metaclass=Meta):
|
|
@staticmethod
|
|
def f():
|
|
return __class__
|
|
|
|
self.assertIs(A, None)
|
|
|
|
B = type("B", (), test_namespace)
|
|
self.assertIs(B.f(), B)
|
|
|
|
@unittest.expectedFailure # TODO: RUSTPYTHON
|
|
def test___class___mro(self):
|
|
# See issue #23722
|
|
test_class = None
|
|
|
|
class Meta(type):
|
|
def mro(self):
|
|
# self.f() doesn't work yet...
|
|
self.__dict__["f"]()
|
|
return super().mro()
|
|
|
|
class A(metaclass=Meta):
|
|
def f():
|
|
nonlocal test_class
|
|
test_class = __class__
|
|
|
|
self.assertIs(test_class, A)
|
|
|
|
def test___classcell___expected_behaviour(self):
|
|
# See issue #23722
|
|
class Meta(type):
|
|
def __new__(cls, name, bases, namespace):
|
|
nonlocal namespace_snapshot
|
|
namespace_snapshot = namespace.copy()
|
|
return super().__new__(cls, name, bases, namespace)
|
|
|
|
# __classcell__ is injected into the class namespace by the compiler
|
|
# when at least one method needs it, and should be omitted otherwise
|
|
namespace_snapshot = None
|
|
class WithoutClassRef(metaclass=Meta):
|
|
pass
|
|
self.assertNotIn("__classcell__", namespace_snapshot)
|
|
|
|
# With zero-arg super() or an explicit __class__ reference,
|
|
# __classcell__ is the exact cell reference to be populated by
|
|
# type.__new__
|
|
namespace_snapshot = None
|
|
class WithClassRef(metaclass=Meta):
|
|
def f(self):
|
|
return __class__
|
|
|
|
class_cell = namespace_snapshot["__classcell__"]
|
|
method_closure = WithClassRef.f.__closure__
|
|
self.assertEqual(len(method_closure), 1)
|
|
self.assertIs(class_cell, method_closure[0])
|
|
# Ensure the cell reference *doesn't* get turned into an attribute
|
|
with self.assertRaises(AttributeError):
|
|
WithClassRef.__classcell__
|
|
|
|
def test___classcell___missing(self):
|
|
# See issue #23722
|
|
# Some metaclasses may not pass the original namespace to type.__new__
|
|
# We test that case here by forcibly deleting __classcell__
|
|
class Meta(type):
|
|
def __new__(cls, name, bases, namespace):
|
|
namespace.pop('__classcell__', None)
|
|
return super().__new__(cls, name, bases, namespace)
|
|
|
|
# The default case should continue to work without any errors
|
|
class WithoutClassRef(metaclass=Meta):
|
|
pass
|
|
|
|
# With zero-arg super() or an explicit __class__ reference, we expect
|
|
# __build_class__ to raise a RuntimeError complaining that
|
|
# __class__ was not set, and asking if __classcell__ was propagated
|
|
# to type.__new__.
|
|
expected_error = '__class__ not set.*__classcell__ propagated'
|
|
with self.assertRaisesRegex(RuntimeError, expected_error):
|
|
class WithClassRef(metaclass=Meta):
|
|
def f(self):
|
|
return __class__
|
|
|
|
def test___classcell___overwrite(self):
|
|
# See issue #23722
|
|
# Overwriting __classcell__ with nonsense is explicitly prohibited
|
|
class Meta(type):
|
|
def __new__(cls, name, bases, namespace, cell):
|
|
namespace['__classcell__'] = cell
|
|
return super().__new__(cls, name, bases, namespace)
|
|
|
|
for bad_cell in (None, 0, "", object()):
|
|
with self.subTest(bad_cell=bad_cell):
|
|
with self.assertRaises(TypeError):
|
|
class A(metaclass=Meta, cell=bad_cell):
|
|
pass
|
|
|
|
def test___classcell___wrong_cell(self):
|
|
# See issue #23722
|
|
# Pointing the cell reference at the wrong class is also prohibited
|
|
class Meta(type):
|
|
def __new__(cls, name, bases, namespace):
|
|
cls = super().__new__(cls, name, bases, namespace)
|
|
B = type("B", (), namespace)
|
|
return cls
|
|
|
|
with self.assertRaises(TypeError):
|
|
class A(metaclass=Meta):
|
|
def f(self):
|
|
return __class__
|
|
|
|
def test_obscure_super_errors(self):
|
|
def f():
|
|
super()
|
|
with self.assertRaisesRegex(RuntimeError, r"no arguments"):
|
|
f()
|
|
|
|
class C:
|
|
def f():
|
|
super()
|
|
with self.assertRaisesRegex(RuntimeError, r"no arguments"):
|
|
C.f()
|
|
|
|
def f(x):
|
|
del x
|
|
super()
|
|
with self.assertRaisesRegex(RuntimeError, r"arg\[0\] deleted"):
|
|
f(None)
|
|
|
|
class X:
|
|
def f(x):
|
|
nonlocal __class__
|
|
del __class__
|
|
super()
|
|
with self.assertRaisesRegex(RuntimeError, r"empty __class__ cell"):
|
|
X().f()
|
|
|
|
def test_cell_as_self(self):
|
|
class X:
|
|
def meth(self):
|
|
super()
|
|
|
|
def f():
|
|
k = X()
|
|
def g():
|
|
return k
|
|
return g
|
|
c = f().__closure__[0]
|
|
self.assertRaises(TypeError, X.meth, c)
|
|
|
|
def test_super_init_leaks(self):
|
|
# Issue #26718: super.__init__ leaked memory if called multiple times.
|
|
# This will be caught by regrtest.py -R if this leak.
|
|
# NOTE: Despite the use in the test a direct call of super.__init__
|
|
# is not endorsed.
|
|
sp = super(float, 1.0)
|
|
for i in range(1000):
|
|
super.__init__(sp, int, i)
|
|
|
|
def test_super_argcount(self):
|
|
with self.assertRaisesRegex(TypeError, "expected at most"):
|
|
super(int, int, int)
|
|
|
|
def test_super_argtype(self):
|
|
with self.assertRaisesRegex(TypeError, "argument 1 must be a type"):
|
|
super(1, int)
|
|
|
|
def test_shadowed_global(self):
|
|
source = textwrap.dedent(
|
|
"""
|
|
class super:
|
|
msg = "truly super"
|
|
|
|
class C:
|
|
def method(self):
|
|
return super().msg
|
|
""",
|
|
)
|
|
with import_helper.ready_to_import(name="shadowed_super", source=source):
|
|
import shadowed_super
|
|
self.assertEqual(shadowed_super.C().method(), "truly super")
|
|
import_helper.unload("shadowed_super")
|
|
|
|
def test_shadowed_local(self):
|
|
class super:
|
|
msg = "quite super"
|
|
|
|
class C:
|
|
def method(self):
|
|
return super().msg
|
|
|
|
self.assertEqual(C().method(), "quite super")
|
|
|
|
def test_shadowed_dynamic(self):
|
|
class MySuper:
|
|
msg = "super super"
|
|
|
|
class C:
|
|
def method(self):
|
|
return super().msg
|
|
|
|
with patch(f"{__name__}.super", MySuper) as m:
|
|
self.assertEqual(C().method(), "super super")
|
|
|
|
def test_shadowed_dynamic_two_arg(self):
|
|
call_args = []
|
|
class MySuper:
|
|
def __init__(self, *args):
|
|
call_args.append(args)
|
|
msg = "super super"
|
|
|
|
class C:
|
|
def method(self):
|
|
return super(1, 2).msg
|
|
|
|
with patch(f"{__name__}.super", MySuper) as m:
|
|
self.assertEqual(C().method(), "super super")
|
|
self.assertEqual(call_args, [(1, 2)])
|
|
|
|
def test_attribute_error(self):
|
|
class C:
|
|
def method(self):
|
|
return super().msg
|
|
|
|
with self.assertRaisesRegex(AttributeError, "'super' object has no attribute 'msg'"):
|
|
C().method()
|
|
|
|
def test_bad_first_arg(self):
|
|
class C:
|
|
def method(self):
|
|
return super(1, self).method()
|
|
|
|
with self.assertRaisesRegex(TypeError, "argument 1 must be a type"):
|
|
C().method()
|
|
|
|
def test_supercheck_fail(self):
|
|
class C:
|
|
def method(self, type_, obj):
|
|
return super(type_, obj).method()
|
|
|
|
c = C()
|
|
err_msg = (
|
|
r"super\(type, obj\): obj \({} {}\) is not "
|
|
r"an instance or subtype of type \({}\)."
|
|
)
|
|
|
|
cases = (
|
|
(int, c, int.__name__, C.__name__, "instance of"),
|
|
# obj is instance of type
|
|
(C, list(), C.__name__, list.__name__, "instance of"),
|
|
# obj is type itself
|
|
(C, list, C.__name__, list.__name__, "type"),
|
|
)
|
|
|
|
for case in cases:
|
|
with self.subTest(case=case):
|
|
type_, obj, type_str, obj_str, instance_or_type = case
|
|
regex = err_msg.format(instance_or_type, obj_str, type_str)
|
|
|
|
with self.assertRaisesRegex(TypeError, regex):
|
|
c.method(type_, obj)
|
|
|
|
def test_super___class__(self):
|
|
class C:
|
|
def method(self):
|
|
return super().__class__
|
|
|
|
self.assertEqual(C().method(), super)
|
|
|
|
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: type 'super' is not an acceptable base type
|
|
def test_super_subclass___class__(self):
|
|
class mysuper(super):
|
|
pass
|
|
|
|
class C:
|
|
def method(self):
|
|
return mysuper(C, self).__class__
|
|
|
|
self.assertEqual(C().method(), mysuper)
|
|
|
|
def test_unusual_getattro(self):
|
|
class MyType(type):
|
|
pass
|
|
|
|
def test(name):
|
|
mytype = MyType(name, (MyType,), {})
|
|
super(MyType, type(mytype)).__setattr__(mytype, "bar", 1)
|
|
self.assertEqual(mytype.bar, 1)
|
|
|
|
for _ in range(ADAPTIVE_WARMUP_DELAY):
|
|
test("foo1")
|
|
|
|
def test_reassigned_new(self):
|
|
class A:
|
|
def __new__(cls):
|
|
pass
|
|
|
|
def __init_subclass__(cls):
|
|
if "__new__" not in cls.__dict__:
|
|
cls.__new__ = cls.__new__
|
|
|
|
class B(A):
|
|
pass
|
|
|
|
class C(B):
|
|
def __new__(cls):
|
|
return super().__new__(cls)
|
|
|
|
for _ in range(ADAPTIVE_WARMUP_DELAY):
|
|
C()
|
|
|
|
def test_mixed_staticmethod_hierarchy(self):
|
|
# This test is just a desugared version of `test_reassigned_new`
|
|
class A:
|
|
@staticmethod
|
|
def some(cls, *args, **kwargs):
|
|
self.assertFalse(args)
|
|
self.assertFalse(kwargs)
|
|
|
|
class B(A):
|
|
def some(cls, *args, **kwargs):
|
|
return super().some(cls, *args, **kwargs)
|
|
|
|
class C(B):
|
|
@staticmethod
|
|
def some(cls):
|
|
return super().some(cls)
|
|
|
|
for _ in range(ADAPTIVE_WARMUP_DELAY):
|
|
C.some(C)
|
|
|
|
@threading_helper.requires_working_threading()
|
|
def test___class___modification_multithreaded(self):
|
|
""" Note: this test isn't actually testing anything on its own.
|
|
It requires a sys audithook to be set to crash on older Python.
|
|
This should be the case anyways as our test suite sets
|
|
an audit hook.
|
|
"""
|
|
|
|
class Foo:
|
|
pass
|
|
|
|
class Bar:
|
|
pass
|
|
|
|
thing = Foo()
|
|
def work():
|
|
foo = thing
|
|
for _ in range(200):
|
|
foo.__class__ = Bar
|
|
type(foo)
|
|
foo.__class__ = Foo
|
|
type(foo)
|
|
|
|
|
|
threads = []
|
|
for _ in range(6):
|
|
thread = threading.Thread(target=work)
|
|
thread.start()
|
|
threads.append(thread)
|
|
|
|
for thread in threads:
|
|
thread.join()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|