#!/usr/bin/env python3
# Copyright (c) Facebook, Inc. and its affiliates. (http://www.facebook.com)
import sys
import unittest
from types import MethodType, ModuleType, SimpleNamespace
from unittest.mock import Mock


class IntepreterTest(unittest.TestCase):
    def test_binary_mul_smallint_returns_large_int(self):
        def mul(a, b):
            return a * b

        # Rewrite mul() to use BINARY_MUL_SMALLINT.
        self.assertEqual(mul(5, 6), 30)

        small_int = 1 << 62
        self.assertEqual(mul(small_int, small_int), small_int * small_int)

    def test_binary_mul_smallint_with_operand_zero_returns_zero(self):
        def mul(a, b):
            return a * b

        # Rewrite mul() to use BINARY_MUL_SMALLINT.
        self.assertEqual(mul(5, 6), 30)

        small_int = 1 << 62
        self.assertEqual(mul(small_int, 0), 0)
        self.assertEqual(mul(0, small_int), 0)

    def test_binary_subscr_calls_type_dunder_class_getitem(self):
        class C:
            def __class_getitem__(cls, item):
                return f"C:{cls.__name__}[{item.__name__}]"

        self.assertEqual(C[int], "C:C[int]")

    def test_binary_subscr_ignores_instance_dunder_class_getitem(self):
        class C:
            def __class_getitem__(cls, item):
                return f"C:{cls.__name__}[{item.__name__}]"

        with self.assertRaises(TypeError) as context:
            C()[int]
        self.assertEqual(str(context.exception), "'C' object is not subscriptable")

    def test_binary_subscr_prioritizes_metaclass_dunder_getitem(self):
        class M(type):
            def __getitem__(cls, item):
                return f"M:{cls.__name__}[{item.__name__}]"

        class C(metaclass=M):
            def __class_getitem__(cls, item):
                return f"C:{cls.__name__}[{item.__name__}]"

        self.assertEqual(C[int], "M:C[int]")

    def test_build_map_returns_dict(self):
        def foo(k0, k1, k2):
            return {k0: 0, k1: 42.4, k2: "test"}

        d = foo("key0", "aaa", "zzz")
        self.assertIs(type(d), dict)
        self.assertEqual(list(d.keys()), ["key0", "aaa", "zzz"])
        self.assertEqual(list(d.values()), [0, 42.4, "test"])

    def test_compare_op_in_propagetes_exception(self):
        class C:
            def __contains__(self, value):
                raise UserWarning("C.__contains__")

        c = C()
        with self.assertRaises(UserWarning) as context:
            1 in c
        self.assertEqual(str(context.exception), "C.__contains__")

    def test_compare_op_not_in_propagetes_exception(self):
        class C:
            def __contains__(self, value):
                raise UserWarning("C.__contains__")

        c = C()
        with self.assertRaises(UserWarning) as context:
            1 not in c
        self.assertEqual(str(context.exception), "C.__contains__")

    def test_for_iter_iterates_dict_by_insertion_order(self):
        d = {}
        d["a"] = 1
        d["c"] = 3
        d["b"] = 2
        result = []
        for key in d:
            result.append(key)
        self.assertEqual(result, ["a", "c", "b"])

    def test_store_name_calls_dunder_setitem(self):
        class C(dict):
            def __setitem__(self, key, value):
                self.result = (key, value)

        locals = C()
        exec("x = 44", None, locals)
        self.assertEqual(locals.result[0], "x")
        self.assertEqual(locals.result[1], 44)

    def test_store_name_setitem_propagates_exception(self):
        class C:
            def __setitem__(self, key, value):
                raise UserWarning("bar")

            def __getitem__(self, key):
                ...  # just here so this type is considered a mapping.

        locals = C()
        with self.assertRaises(UserWarning) as context:
            exec("x = 1", None, locals)
        self.assertEqual(str(context.exception), "bar")

    def test_store_name_setitem_propagates_descriptor_exception(self):
        class D:
            def __get__(self, instance, owner):
                self.result = (instance, owner)
                raise UserWarning("baz")

        d = D()

        class C:
            __setitem__ = d

            def __getitem__(self, key):
                ...  # just here so this type is considered a mapping.

        locals = C()
        with self.assertRaises(UserWarning) as context:
            exec("x = 1", None, locals)
        self.assertEqual(str(context.exception), "baz")
        self.assertEqual(d.result[0], locals)
        self.assertEqual(d.result[1], C)

    def test_load_attr_calls_dunder_get(self):
        class Descriptor:
            def __get__(self, *args):
                self.args = args
                return 42

        descriptor = Descriptor()

        class C:
            foo = descriptor

        self.assertEqual(C.foo, 42)
        self.assertEqual(descriptor.args, (None, C))
        c = C()
        self.assertEqual(c.foo, 42)
        self.assertEqual(descriptor.args, (c, C))

    def test_load_attr_calls_callable_dunder_get(self):
        class Callable:
            def __call__(self, *args):
                self.args = args
                return 42

        callable = Callable()

        class Descriptor:
            __get__ = callable

        descriptor = Descriptor()

        class C:
            foo = descriptor

        self.assertEqual(C.foo, 42)
        self.assertEqual(callable.args, (descriptor, None, C))
        c = C()
        self.assertEqual(c.foo, 42)
        self.assertEqual(callable.args, (descriptor, c, C))

    def test_load_attr_does_not_recursively_call_dunder_get(self):
        class Descriptor0:
            def __get__(self, *args):
                return 13

            def __call__(self, *args):
                return 42

        descriptor0 = Descriptor0()

        class Descriptor1:
            __get__ = descriptor0

        descriptor1 = Descriptor1()

        class C:
            foo = descriptor1

        self.assertEqual(C.foo, 42)
        self.assertEqual(C().foo, 42)
        self.assertIs(C.__dict__["foo"].__get__, 13)

    def test_load_attr_with_module_prioritizes_data_descr_than_instance_attr(self):
        from types import ModuleType

        class MyModule(ModuleType):
            @property
            def foo(self):
                return "data descriptor"

        m = MyModule("m")
        m.__dict__["foo"] = "instance attribute"

        self.assertEqual(m.foo, "data descriptor")

    def test_load_attr_with_module_prioritizes_instance_attr_than_non_data_descr(self):
        from types import ModuleType

        class NonDataDescriptor:
            def __get__(self, obj, obj_type):
                return "non-data descriptor"

        class MyModule(ModuleType):
            foo = NonDataDescriptor()

        m = MyModule("m")
        m.foo = "instance attribute"

        self.assertEqual(m.foo, "instance attribute")

    def test_load_attr_with_module_prioritizes_non_data_descr_than_instance_getattr(
        self,
    ):
        from types import ModuleType

        class NonDataDescriptor:
            def __get__(self, obj, obj_type):
                return "non-data descriptor"

        class MyModule(ModuleType):
            foo = NonDataDescriptor()

        m = MyModule("m")
        exec('def __getattr__(name): return name + " instance.__getattr__"', m.__dict__)
        self.assertEqual(m.foo, "non-data descriptor")

    def test_load_attr_with_module_prioritizes_instance_getattr_than_type_getattr(self):
        from types import ModuleType

        class MyModule(ModuleType):
            def __getattr__(self, name):
                return name + " type.__getattr__"

        m = MyModule("m")
        exec('def __getattr__(name): return name + " instance.__getattr__"', m.__dict__)
        self.assertEqual(m.foo, "foo instance.__getattr__")

    def test_load_attr_with_module_returns_type_getattr(self):
        from types import ModuleType

        class MyModule(ModuleType):
            def __getattr__(self, name):
                return name + " type.__getattr__"

        m = MyModule("m")
        self.assertEqual(m.foo, "foo type.__getattr__")

    def test_load_attr_with_module_returns_type_getattr(self):
        from types import ModuleType

        class MyModule(ModuleType):
            pass

        m = MyModule("m")
        with self.assertRaises(AttributeError):
            m.foo

    def test_load_deref_with_assigned_cell_returns_assigned_value(self):
        def foo(x):
            def bar():
                return x

            return bar

        bar = foo(10)
        self.assertEqual(bar(), 10)

    def test_load_deref_with_delete_cell_raises_name_error(self):
        def foo(x):
            def bar():
                return x  # noqa: F821

            del x
            return bar

        bar = foo(10)
        with self.assertRaises(NameError):
            bar()

    def test_load_name_calls_dunder_getitem(self):
        class C:
            def __getitem__(self, key):
                return (key, 17)

        globals = {"foo": None}
        locals = C()
        exec('assert(foo == ("foo", 17))', globals, locals)

    def test_load_name_continues_on_keyerror(self):
        class C:
            def __getitem__(self, key):
                self.result = key
                raise KeyError()

        globals = {}
        exec("foo = 99", globals)
        self.assertIn("foo", globals)

        locals = C()
        exec("assert(foo == 99)", globals, locals)  # should not raise
        self.assertEqual(locals.result, "foo")

    def test_load_name_getitem_propagates_exception(self):
        class C:
            def __getitem__(self, key):
                raise UserWarning("bar")

        globals = None
        locals = C()
        with self.assertRaises(UserWarning) as context:
            exec("foo", globals, locals)
        self.assertEqual(str(context.exception), "bar")

    def test_load_name_getitem_propagates_descriptor_exception(self):
        class D:
            def __get__(self, instance, owner):
                self.result = (instance, owner)
                raise UserWarning("baz")

        d = D()

        class C:
            __getitem__ = d

        globals = None
        locals = C()
        with self.assertRaises(UserWarning) as context:
            exec("foo", globals, locals)
        self.assertEqual(str(context.exception), "baz")
        self.assertEqual(d.result[0], locals)
        self.assertEqual(d.result[1], C)

    def test_load_global_returns_global(self):
        globals = {
            "foo": 42,
            "__builtins__": {"foo": 8},
        }
        locals = {"foo": 3}
        self.assertIs(
            exec("def get_foo(): global foo; return foo", globals, locals), None
        )
        self.assertEqual(locals["get_foo"](), 42)

    def test_load_global_returns_builtin(self):
        globals = {
            "foo": 42,
            "__builtins__": {"bar": 8},
        }
        locals = {"bar": 3}
        self.assertIs(
            exec("def get_bar(): global bar; return bar", globals, locals), None
        )
        self.assertEqual(locals["get_bar"](), 8)

    def test_load_global_does_not_cache_builtin(self):
        builtins = {"bar": -8}
        globals = {"foo": 42, "__builtins__": builtins}
        locals = {"bar": 3}
        self.assertIs(
            exec("def get_bar(): global bar; return bar", globals, locals), None
        )
        self.assertEqual(locals["get_bar"](), -8)
        builtins["bar"] = 9
        self.assertEqual(locals["get_bar"](), 9)

    def test_load_global_calls_builtins_getitem(self):
        class C:
            def __getitem__(self, key):
                return (self, key)

        builtins = C()
        globals = {"foo": 42, "__builtins__": builtins}
        locals = {"bar": 3}
        self.assertIs(
            exec("def get_bar(): global bar; return bar", globals, locals), None
        )
        self.assertEqual(locals["get_bar"](), (builtins, "bar"))

    def test_delete_attr_calls_dunder_delete(self):
        class Descriptor:
            def __delete__(self, *args):
                self.args = args
                return "return value ignored"

        descriptor = Descriptor()

        class C:
            foo = descriptor

        c = C()
        del c.foo
        self.assertEqual(descriptor.args, (c,))
        # Deletion on class should just delete descriptor
        del C.foo
        self.assertFalse(hasattr(C, "foo"))
        self.assertNotIn("foo", C.__dict__)

    def test_delete_attr_calls_callable_dunder_delete(self):
        class Callable:
            def __call__(self, *args):
                self.args = args
                return "return value ignored"

        callable = Callable()

        class Descriptor:
            __delete__ = callable

        descriptor = Descriptor()

        class C:
            foo = descriptor

        c = C()
        del c.foo
        self.assertEqual(callable.args, (c,))
        # Deletion on class should just delete descriptor
        del C.foo
        self.assertFalse(hasattr(C, "foo"))
        self.assertNotIn("foo", C.__dict__)

    def test_delete_attr_call_dunder_delete_descriptor(self):
        class Callable:
            def __call__(self, *args):
                self.args = args
                return "return value ignored"

        callable = Callable()

        class Descriptor0:
            def __get__(self, *args):
                self.args = args
                return callable

        descriptor0 = Descriptor0()

        class Descriptor1:
            __delete__ = descriptor0

        descriptor1 = Descriptor1()

        class C:
            foo = descriptor1

        c = C()
        del c.foo
        self.assertEqual(descriptor0.args, (descriptor1, Descriptor1))
        self.assertEqual(callable.args, (c,))

    def test_delete_fast_with_assigned_name_succeeds(self):
        def foo():
            a = 4
            del a

        foo()

    def test_delete_fast_with_deleted_name_raises_unbound_local_error(self):
        def foo():
            a = 4
            del a
            del a  # noqa: F821

        with self.assertRaises(UnboundLocalError) as context:
            foo()
        self.assertEqual(
            str(context.exception), "local variable 'a' referenced before assignment"
        )

    def test_delete_name_calls_dunder_delitem(self):
        class C(dict):
            def __delitem__(self, key):
                self.result = key
                return None

        locals = C()
        exec("del foo", None, locals)
        self.assertEqual(locals.result, "foo")

    def test_delete_name_raises_name_error_on_error(self):
        class C(dict):
            def __delitem__(self, key):
                self.result = key
                raise UserWarning("bar")

        globals = {}
        locals = C()
        with self.assertRaises(NameError) as context:
            exec("del foo", globals, locals)
        self.assertEqual(str(context.exception), "name 'foo' is not defined")
        self.assertEqual(locals.result, "foo")

    def test_delete_name_raises_name_error_on_descriptor_error(self):
        class D:
            def __get__(self, instance, owner):
                self.result = (instance, owner)
                raise UserWarning("baz")

        d = D()

        class C(dict):
            __delitem__ = d

        globals = {}
        locals = C()
        with self.assertRaises(NameError) as context:
            exec("del foo", globals, locals)
        self.assertEqual(str(context.exception), "name 'foo' is not defined")
        self.assertEqual(d.result[0], locals)
        self.assertEqual(d.result[1], C)

    def test_get_iter_with_range_with_bool(self):
        result = None
        for i in range(True):
            if result is None:
                result = i
            else:
                result += i
        self.assertEqual(result, 0)

        result = 0
        for i in range(True, 4):
            result += i
        self.assertEqual(result, 6)

        result = 0
        for i in range(0, 4, True):
            result += i
        self.assertEqual(result, 6)

    def test_import_performs_secondary_lookup(self):
        import sys

        class FakeModule:
            def __getattribute__(self, name):
                if name == "__name__":
                    return "test_import_performs_secondary_lookup_fake_too"
                raise AttributeError(name)

        sys.modules["test_import_performs_secondary_lookup_fake"] = FakeModule()
        sys.modules["test_import_performs_secondary_lookup_fake_too.foo"] = 42
        try:
            from test_import_performs_secondary_lookup_fake import foo
        finally:
            del sys.modules["test_import_performs_secondary_lookup_fake"]
            del sys.modules["test_import_performs_secondary_lookup_fake_too.foo"]
        self.assertEqual(foo, 42)

    def test_call_ex_kw_with_class_with_iterable_raises_type_error(self):
        def foo(a):
            return a

        mapping = [("hello", "world")]
        with self.assertRaisesRegex(TypeError, "must be a mapping, not list"):
            foo(**mapping)

    def test_call_ex_kw_with_class_without_keys_method_raises_type_error(self):
        class C:
            __getitem__ = Mock(name="__getitem__", return_value="mock")

        def foo(a):
            return a

        mapping = C()
        with self.assertRaisesRegex(TypeError, "must be a mapping, not C"):
            foo(**mapping)

    def test_call_ex_kw_with_dict_subclass_does_not_call_keys_or_dunder_getitem(self):
        class C(dict):
            __getitem__ = Mock(name="__getitem__", return_value="mock")
            keys = Mock(name="keys", return_value=("a",))

        def foo(a):
            return a

        mapping = C({"a": "foo"})
        result = foo(**mapping)
        self.assertEqual(result, "foo")
        C.keys.assert_not_called()
        C.__getitem__.assert_not_called()

    def test_call_ex_kw_with_non_dict_calls_keys_and_dunder_getitem(self):
        class C:
            __getitem__ = Mock(name="__getitem__", return_value="mock")
            keys = Mock(name="keys", return_value=("a",))

        def foo(a):
            return a

        mapping = C()
        result = foo(**mapping)
        self.assertEqual(result, "mock")
        C.keys.assert_called_once()
        C.__getitem__.assert_called_once()

    def test_call_ex_kw_with_non_dict_passes_dict_as_kwargs(self):
        class C:
            __getitem__ = Mock(name="__getitem__", return_value="mock")
            keys = Mock(name="keys", return_value=("hello", "world"))

        def foo(**kwargs):
            self.assertIs(type(kwargs), dict)
            self.assertIn("hello", kwargs)
            self.assertIn("world", kwargs)

        mapping = C()
        foo(**mapping)
        C.keys.assert_called_once()
        self.assertEqual(C.__getitem__.call_count, 2)

    def test_callfunction_kw_does_not_modify_actual_names(self):
        def foo(a, b):
            return (a, b)

        def bar():
            return foo(b="b", a="a")

        # Call the same function twice to see the actual names are preserved.
        self.assertEqual(bar(), ("a", "b"))
        self.assertEqual(bar(), ("a", "b"))

    def test_call_raises_recursion_error(self):
        def foo(x):
            return foo(x)

        with self.assertRaisesRegex(RecursionError, "maximum recursion depth exceeded"):
            foo(1)

    def test_cache_misses_after_dunder_class_update(self):
        class C:
            def foo(self):
                return 100

        class D:
            def foo(self):
                return 200

        def cache_attribute(c):
            return c.foo()

        c = C()
        # Load the cache
        result = cache_attribute(c)
        self.assertIs(result, 100)

        c.__class__ = D
        # The loaded cache doesn't match `c` since its layout id has changed.
        result = cache_attribute(c)
        self.assertIs(result, 200)

    def test_unpack_sequence_raises_type_error(self):
        class Foo:
            pass

        with self.assertRaisesRegex(TypeError, "cannot unpack non-iterable Foo object"):
            a, b = Foo()


class InlineCacheTests(unittest.TestCase):
    def test_load_slot_descriptor(self):
        class C:
            __slots__ = "x"

        def read_x(c):
            return c.x

        c = C()
        c.x = 50

        # Cache C.x.
        self.assertEqual(read_x(c), 50)
        # Use the cached descriptor.
        self.assertEqual(read_x(c), 50)

        # The cached descriptor raises AttributeError after its corresponding
        # attribute gets deleted.
        del c.x
        with self.assertRaises(AttributeError):
            read_x(c)

    def test_load_slot_descriptor_invalidated_by_updating_type_attr(self):
        class C:
            __slots__ = "x"

        def read_x(c):
            return c.x

        c = C()
        c.x = 50

        # Cache C.x.
        self.assertEqual(read_x(c), 50)

        # Invalidate the cache.
        C.x = property(lambda self: 100)

        # Verify that the updated type attribute is used for `c.x`.
        self.assertEqual(read_x(c), 100)

    def test_store_attr_calls_dunder_set(self):
        class Descriptor:
            def __set__(self, *args):
                self.args = args
                return "return value ignored"

        descriptor = Descriptor()

        class C:
            foo = descriptor

        c = C()
        c.foo = 42
        self.assertEqual(descriptor.args, (c, 42))
        # Assignment on the class should replace descriptor.
        C.foo = 13
        self.assertEqual(C.foo, 13)
        self.assertEqual(C.__dict__["foo"], 13)

    def test_store_attr_calls_callable_dunder_set(self):
        class Callable:
            def __call__(self, *args):
                self.args = args
                return "return value ignored"

        callable = Callable()

        class Descriptor:
            __set__ = callable

        descriptor = Descriptor()

        class C:
            foo = descriptor

        c = C()
        c.foo = 42
        self.assertEqual(callable.args, (c, 42))
        # Assignment on the class should replace descriptor.
        C.foo = 13
        self.assertEqual(C.foo, 13)
        self.assertEqual(C.__dict__["foo"], 13)

    def test_store_attr_call_dunder_set_descriptor(self):
        class Callable:
            def __call__(self, *args):
                self.args = args
                return "return value ignored"

        callable = Callable()

        class Descriptor0:
            def __get__(self, *args):
                self.args = args
                return callable

        descriptor0 = Descriptor0()

        class Descriptor1:
            __set__ = descriptor0

        descriptor1 = Descriptor1()

        class C:
            foo = descriptor1

        c = C()
        c.foo = 42
        self.assertEqual(descriptor0.args, (descriptor1, Descriptor1))
        self.assertEqual(callable.args, (c, 42))

    def test_store_slot_descriptor(self):
        class C:
            __slots__ = "x"

        def write_x(c, v):
            c.x = v

        c = C()

        # Cache C.x.
        write_x(c, 1)
        self.assertEqual(c.x, 1)

        # Use the cached descriptor.
        write_x(c, 2)
        self.assertEqual(c.x, 2)

    def test_store_slot_descriptor_invalidated_by_updating_type_attr(self):
        class C:
            __slots__ = "x"

        def write_x(c, v):
            c.x = v

        c = C()

        # Cache C.x.
        write_x(c, 1)
        self.assertEqual(c.x, 1)

        # Invalidate the cache.
        def setter(self, value):
            value["setter_executed"] = True

        C.x = property(lambda self: 100, setter)

        # Verify that the updated type attribute is used for `c.x`.
        v = {"setter_executed": False}
        write_x(c, v)
        self.assertTrue(v["setter_executed"], True)

    def test_raise_oserror_with_errno_and_strerror(self):
        with self.assertRaises(FileNotFoundError) as context:
            open("/tmp/nonexisting_file")
        exc = context.exception
        self.assertEqual(exc.errno, 2)
        self.assertEqual(exc.strerror, "No such file or directory")
        # TODO(T67323177): Check filename.

    def test_call_with_bound_method_storing_arbitrary_callable_resolves_callable(self):
        class C:
            def __call__(self, arg):
                return self, arg

        func = C()
        instance = object()
        method = MethodType(func, instance)
        self.assertEqual((func, instance), method())


class ImportNameTests(unittest.TestCase):
    def test_import_name_calls_dunder_builtins_dunder_import(self):
        def my_import(name, globals, locals, fromlist, level):
            return SimpleNamespace(baz=42, bam=(name, fromlist, level))

        builtins = ModuleType("builtins")
        builtins.__import__ = my_import
        my_globals = {"__builtins__": builtins}

        exec("from ..foo.bar import baz, bam", my_globals)
        self.assertEqual(my_globals["baz"], 42)
        self.assertEqual(my_globals["bam"], ("foo.bar", ("baz", "bam"), 2))

    def test_import_name_without_dunder_import_raises_import_error(self):
        my_builtins = ModuleType("my_builtins")
        my_globals = {"__builtins__": my_builtins}
        with self.assertRaisesRegex(ImportError, "__import__ not found"):
            exec("import foo", my_globals)

    def test_import_from_module_with_raising_dunder_getattr_propagates_exception(self):
        import collections

        self.assertIn("__getattr__", collections.__dict__)
        with self.assertRaises(ImportError):
            from collections import definitely_does_not_exist


class ImportStarTests(unittest.TestCase):
    def test_import_star_imports_symbols(self):
        def my_import(name, globals, locals, fromlist, level):
            result = ModuleType("m")
            result.foo = 1
            result._bar = 2
            result.baz_ = 3
            result.__bam__ = 4
            return result

        builtins = ModuleType("builtins")
        builtins.__import__ = my_import
        my_globals = {"__builtins__": builtins}

        exec("from m import *", my_globals)
        self.assertEqual(my_globals["foo"], 1)
        self.assertEqual(my_globals["baz_"], 3)
        self.assertNotIn("_bar", my_globals)
        self.assertNotIn("__bam__", my_globals)

    def test_import_star_with_dunder_all_tuple_imports_symbols(self):
        def my_import(name, globals, locals, fromlist, level):
            result = ModuleType("m")
            result.foo = 1
            result._bar = 2
            result.baz = 99
            result.__all__ = ("foo", "_bar", "__repr__")
            return result

        builtins = ModuleType("builtins")
        builtins.__import__ = my_import
        my_globals = {"__builtins__": builtins}

        exec("from m import *", my_globals)
        self.assertEqual(my_globals["foo"], 1)
        self.assertEqual(my_globals["_bar"], 2)
        self.assertEqual(my_globals["__repr__"](), "<module 'm'>")
        self.assertNotIn("baz", my_globals)
        self.assertNotIn("__all__", my_globals)

    def test_import_star_with_dunder_all_list_imports_symbols(self):
        def my_import(name, globals, locals, fromlist, level):
            result = ModuleType("m")
            result.foo = 1
            result._bar = 2
            result.baz = 99
            result.__all__ = ["foo", "_bar", "__all__"]
            return result

        builtins = ModuleType("builtins")
        builtins.__import__ = my_import
        my_globals = {"__builtins__": builtins}

        exec("from m import *", my_globals)
        self.assertEqual(my_globals["foo"], 1)
        self.assertEqual(my_globals["_bar"], 2)
        self.assertEqual(my_globals["__all__"], ["foo", "_bar", "__all__"])
        self.assertNotIn("baz", my_globals)

    def test_import_star_with_dunder_all_sequence_imports_symbols(self):
        def my_import(name, globals, locals, fromlist, level):
            class C:
                def __getitem__(self, key):
                    if key == 0:
                        return "foo"
                    if key == 1:
                        return "_bar"
                    raise IndexError

            result = ModuleType("m")
            result.foo = 1
            result._bar = 2
            result.baz = 99
            result.__all__ = C()
            return result

        builtins = ModuleType("builtins")
        builtins.__import__ = my_import
        my_globals = {"__builtins__": builtins}

        exec("from m import *", my_globals)
        self.assertEqual(my_globals["foo"], 1)
        self.assertEqual(my_globals["_bar"], 2)
        self.assertNotIn("baz", my_globals)

    def test_import_star_calls_object_dunder_dict_keys(self):
        def my_import(name, globals, locals, fromlist, level):
            class C:
                def keys(self):
                    return ["foo", "_bar", "baz_"]

            class M:
                __dict__ = C()

                def __getattr__(self, key):
                    if key == "__all__":
                        raise AttributeError
                    return f"value for {key}"

            return M()

        builtins = ModuleType("builtins")
        builtins.__import__ = my_import
        my_globals = {"__builtins__": builtins}

        exec("from m import *", my_globals)
        self.assertEqual(my_globals["foo"], "value for foo")
        self.assertEqual(my_globals["baz_"], "value for baz_")
        self.assertNotIn("_bar", my_globals)

    def test_import_star_calls_implicit_globals_dunder_setitem(self):
        def my_import(name, globals, locals, fromlist, level):
            result = ModuleType("m")
            result.foo = 88
            result.bar = 77
            return result

        class G(dict):
            def __init__(self):
                self.setitem_keys = []

            def __setitem__(self, key, value):
                dict.__setitem__(self, key, value)
                self.setitem_keys.append(key)

        builtins = ModuleType("builtins")
        builtins.__import__ = my_import
        my_globals = {"__builtins__": builtins}
        my_locals = G()

        exec("from m import *", my_globals, my_locals)
        self.assertEqual(my_locals["foo"], 88)
        self.assertEqual(my_locals["bar"], 77)
        self.assertEqual(sorted(my_locals.setitem_keys), ["bar", "foo"])
        self.assertNotIn("foo", my_globals)
        self.assertNotIn("bar", my_globals)

    def test_import_star_with_object_without_dunder_dict_raises_import_errro(self):
        def my_import(name, globals, locals, fromlist, level):
            class C:
                def __getattribute__(self, key):
                    if key == "__dict__":
                        raise AttributeError
                    return object.__getattribute__(self, key)

            return C()

        builtins = ModuleType("builtins")
        builtins.__import__ = my_import
        my_globals = {"__builtins__": builtins}

        with self.assertRaisesRegex(
            ImportError, r"from-import-\* object has no __dict__ and no __all__"
        ):
            exec("from m import *", my_globals)


if __name__ == "__main__":
    unittest.main()
