diff --git a/Lib/test/support.py b/Lib/test/support.py index a05f420710c..28f7e2766dc 100644 --- a/Lib/test/support.py +++ b/Lib/test/support.py @@ -30,7 +30,8 @@ __all__ = ["Error", "TestFailed", "ResourceDenied", "import_module", "run_with_locale", "set_memlimit", "bigmemtest", "bigaddrspacetest", "BasicTestRunner", "run_unittest", "run_doctest", "threading_setup", "threading_cleanup", - "reap_children", "cpython_only", "check_impl_detail", "get_attribute"] + "reap_children", "cpython_only", "check_impl_detail", "get_attribute", + "swap_item", "swap_attr"] class Error(Exception): """Base class for regression test exceptions.""" @@ -1074,3 +1075,57 @@ def reap_children(): break except: break + +@contextlib.contextmanager +def swap_attr(obj, attr, new_val): + """Temporary swap out an attribute with a new object. + + Usage: + with swap_attr(obj, "attr", 5): + ... + + This will set obj.attr to 5 for the duration of the with: block, + restoring the old value at the end of the block. If `attr` doesn't + exist on `obj`, it will be created and then deleted at the end of the + block. + """ + if hasattr(obj, attr): + real_val = getattr(obj, attr) + setattr(obj, attr, new_val) + try: + yield + finally: + setattr(obj, attr, real_val) + else: + setattr(obj, attr, new_val) + try: + yield + finally: + delattr(obj, attr) + +@contextlib.contextmanager +def swap_item(obj, item, new_val): + """Temporary swap out an item with a new object. + + Usage: + with swap_item(obj, "item", 5): + ... + + This will set obj["item"] to 5 for the duration of the with: block, + restoring the old value at the end of the block. If `item` doesn't + exist on `obj`, it will be created and then deleted at the end of the + block. + """ + if item in obj: + real_val = obj[item] + obj[item] = new_val + try: + yield + finally: + obj[item] = real_val + else: + obj[item] = new_val + try: + yield + finally: + del obj[item] diff --git a/Lib/test/test_dynamic.py b/Lib/test/test_dynamic.py new file mode 100644 index 00000000000..beb7b1cdf1c --- /dev/null +++ b/Lib/test/test_dynamic.py @@ -0,0 +1,143 @@ +# Test the most dynamic corner cases of Python's runtime semantics. + +import builtins +import contextlib +import unittest + +from test.support import run_unittest, swap_item, swap_attr + + +class RebindBuiltinsTests(unittest.TestCase): + + """Test all the ways that we can change/shadow globals/builtins.""" + + def configure_func(self, func, *args): + """Perform TestCase-specific configuration on a function before testing. + + By default, this does nothing. Example usage: spinning a function so + that a JIT will optimize it. Subclasses should override this as needed. + + Args: + func: function to configure. + *args: any arguments that should be passed to func, if calling it. + + Returns: + Nothing. Work will be performed on func in-place. + """ + pass + + def test_globals_shadow_builtins(self): + # Modify globals() to shadow an entry in builtins. + def foo(): + return len([1, 2, 3]) + self.configure_func(foo) + + self.assertEqual(foo(), 3) + with swap_item(globals(), "len", lambda x: 7): + self.assertEqual(foo(), 7) + + def test_modify_builtins(self): + # Modify the builtins module directly. + def foo(): + return len([1, 2, 3]) + self.configure_func(foo) + + self.assertEqual(foo(), 3) + with swap_attr(builtins, "len", lambda x: 7): + self.assertEqual(foo(), 7) + + def test_modify_builtins_while_generator_active(self): + # Modify the builtins out from under a live generator. + def foo(): + x = range(3) + yield len(x) + yield len(x) + self.configure_func(foo) + + g = foo() + self.assertEqual(next(g), 3) + with swap_attr(builtins, "len", lambda x: 7): + self.assertEqual(next(g), 7) + + def test_modify_builtins_from_leaf_function(self): + # Verify that modifications made by leaf functions percolate up the + # callstack. + with swap_attr(builtins, "len", len): + def bar(): + builtins.len = lambda x: 4 + + def foo(modifier): + l = [] + l.append(len(range(7))) + modifier() + l.append(len(range(7))) + return l + self.configure_func(foo, lambda: None) + + self.assertEqual(foo(bar), [7, 4]) + + def test_cannot_change_globals_or_builtins_with_eval(self): + def foo(): + return len([1, 2, 3]) + self.configure_func(foo) + + # Note that this *doesn't* change the definition of len() seen by foo(). + builtins_dict = {"len": lambda x: 7} + globals_dict = {"foo": foo, "__builtins__": builtins_dict, + "len": lambda x: 8} + self.assertEqual(eval("foo()", globals_dict), 3) + + self.assertEqual(eval("foo()", {"foo": foo}), 3) + + def test_cannot_change_globals_or_builtins_with_exec(self): + def foo(): + return len([1, 2, 3]) + self.configure_func(foo) + + globals_dict = {"foo": foo} + exec("x = foo()", globals_dict) + self.assertEqual(globals_dict["x"], 3) + + # Note that this *doesn't* change the definition of len() seen by foo(). + builtins_dict = {"len": lambda x: 7} + globals_dict = {"foo": foo, "__builtins__": builtins_dict, + "len": lambda x: 8} + + exec("x = foo()", globals_dict) + self.assertEqual(globals_dict["x"], 3) + + def test_cannot_replace_builtins_dict_while_active(self): + def foo(): + x = range(3) + yield len(x) + yield len(x) + self.configure_func(foo) + + g = foo() + self.assertEqual(next(g), 3) + with swap_item(globals(), "__builtins__", {"len": lambda x: 7}): + self.assertEqual(next(g), 3) + + def test_cannot_replace_builtins_dict_between_calls(self): + def foo(): + return len([1, 2, 3]) + self.configure_func(foo) + + self.assertEqual(foo(), 3) + with swap_item(globals(), "__builtins__", {"len": lambda x: 7}): + self.assertEqual(foo(), 3) + + def test_eval_gives_lambda_custom_globals(self): + globals_dict = {"len": lambda x: 7} + foo = eval("lambda: len([])", globals_dict) + self.configure_func(foo) + + self.assertEqual(foo(), 7) + + +def test_main(): + run_unittest(RebindBuiltinsTests) + + +if __name__ == "__main__": + test_main()