import contextlib
import unittest
from collections import OrderedDict, UserDict
from types import MappingProxyType
from test import support
from test.support import import_helper


_testcapi = import_helper.import_module("_testcapi")
_testlimitedcapi = import_helper.import_module("_testlimitedcapi")


NULL = None
INVALID_UTF8 = b'\xff'

class DictSubclass(dict):
    def __getitem__(self, key):
        raise RuntimeError('do not get evil')
    def __setitem__(self, key, value):
        raise RuntimeError('do not set evil')
    def __delitem__(self, key):
        raise RuntimeError('do not del evil')

def gen():
    yield 'a'
    yield 'b'
    yield 'c'


class FrozenDictSubclass(frozendict):
    pass


DICT_TYPES = (dict, DictSubclass, OrderedDict)
FROZENDICT_TYPES = (frozendict, FrozenDictSubclass)
ANYDICT_TYPES = DICT_TYPES + FROZENDICT_TYPES
MAPPING_TYPES = (UserDict,)
NOT_DICT_TYPES = FROZENDICT_TYPES + MAPPING_TYPES
NOT_FROZENDICT_TYPES = DICT_TYPES + MAPPING_TYPES
NOT_ANYDICT_TYPES = MAPPING_TYPES
OTHER_TYPES = (lambda: [1], lambda: 42, object)  # (list, int, object)


class CAPITest(unittest.TestCase):

    def test_dict_check(self):
        # Test PyDict_Check()
        check = _testlimitedcapi.dict_check
        for dict_type in DICT_TYPES:
            self.assertTrue(check(dict_type({1: 2})))
        for test_type in NOT_DICT_TYPES:
            self.assertFalse(check(test_type({1: 2})))
        for test_type in OTHER_TYPES:
            self.assertFalse(check(test_type()))
        # CRASHES check(NULL)

    def test_dict_checkexact(self):
        # Test PyDict_CheckExact()
        check = _testlimitedcapi.dict_checkexact
        for dict_type in DICT_TYPES:
            self.assertEqual(check(dict_type({1: 2})), dict_type == dict)
        for test_type in NOT_DICT_TYPES:
            self.assertFalse(check(test_type({1: 2})))
        for test_type in OTHER_TYPES:
            self.assertFalse(check(test_type()))
        # CRASHES check(NULL)

    def test_dict_new(self):
        # Test PyDict_New()
        dict_new = _testlimitedcapi.dict_new
        dct = dict_new()
        self.assertEqual(dct, {})
        self.assertIs(type(dct), dict)
        dct2 = dict_new()
        self.assertIsNot(dct2, dct)

    def test_dictproxy_new(self):
        # Test PyDictProxy_New()
        dictproxy_new = _testlimitedcapi.dictproxy_new
        for dict_type in ANYDICT_TYPES + MAPPING_TYPES:
            if dict_type == DictSubclass:
                continue
            dct = dict_type({1: 2})
            proxy = dictproxy_new(dct)
            self.assertIs(type(proxy), MappingProxyType)
            self.assertEqual(proxy, dct)
            with self.assertRaises(TypeError):
                proxy[1] = 3
            self.assertEqual(proxy[1], 2)
            if not isinstance(dct, frozendict):
                dct[1] = 4
                self.assertEqual(proxy[1], 4)

        self.assertRaises(TypeError, dictproxy_new, [])
        self.assertRaises(TypeError, dictproxy_new, 42)
        # CRASHES dictproxy_new(NULL)

    def test_dict_copy(self):
        # Test PyDict_Copy()
        copy = _testlimitedcapi.dict_copy
        for dict_type in DICT_TYPES:
            dct = dict_type({1: 2})
            dct_copy = copy(dct)
            self.assertIs(type(dct_copy), dict)
            self.assertEqual(dct_copy, dct)

        for test_type in NOT_DICT_TYPES + OTHER_TYPES:
            self.assertRaises(SystemError, copy, test_type())
        self.assertRaises(SystemError, copy, NULL)

    def test_dict_clear(self):
        # Test PyDict_Clear()
        clear = _testlimitedcapi.dict_clear
        for dict_type in DICT_TYPES:
            if dict_type == OrderedDict:
                # NOTE: It is not safe to call it with OrderedDict.
                continue
            dct = dict_type({1: 2})
            clear(dct)
            self.assertEqual(dct, {})

        # Has no effect for non-dicts.
        for test_type in NOT_DICT_TYPES:
            dct = test_type({1: 2})
            clear(dct)
            self.assertEqual(dct, {1: 2})
        lst = [1, 2]
        clear(lst)
        self.assertEqual(lst, [1, 2])
        clear(object())

        # CRASHES clear(NULL)

    def test_dict_size(self):
        # Test PyDict_Size()
        size = _testlimitedcapi.dict_size
        for dict_type in ANYDICT_TYPES:
            self.assertEqual(size(dict_type({1: 2, 3: 4})), 2)

        for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES:
            self.assertRaises(SystemError, size, test_type())
        self.assertRaises(SystemError, size, NULL)

    def test_dict_getitem(self):
        # Test PyDict_GetItem()
        getitem = _testlimitedcapi.dict_getitem
        for dict_type in ANYDICT_TYPES:
            dct = dict_type({'a': 1, '\U0001f40d': 2})
            self.assertEqual(getitem(dct, 'a'), 1)
            self.assertIs(getitem(dct, 'b'), KeyError)
            self.assertEqual(getitem(dct, '\U0001f40d'), 2)

        with support.catch_unraisable_exception() as cm:
            self.assertIs(getitem({}, []), KeyError)  # unhashable
            self.assertEqual(cm.unraisable.exc_type, TypeError)
            self.assertEqual(str(cm.unraisable.exc_value),
                             "unhashable type: 'list'")

        for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES:
            self.assertIs(getitem(test_type(), 'a'), KeyError)

        # CRASHES getitem({}, NULL)
        # CRASHES getitem(NULL, 'a')

    def test_dict_getitemstring(self):
        # Test PyDict_GetItemString()
        getitemstring = _testlimitedcapi.dict_getitemstring
        for dict_type in ANYDICT_TYPES:
            dct = dict_type({'a': 1, '\U0001f40d': 2})
            self.assertEqual(getitemstring(dct, b'a'), 1)
            self.assertIs(getitemstring(dct, b'b'), KeyError)
            self.assertEqual(getitemstring(dct, '\U0001f40d'.encode()), 2)

        with support.catch_unraisable_exception() as cm:
            self.assertIs(getitemstring({}, INVALID_UTF8), KeyError)
            self.assertEqual(cm.unraisable.exc_type, UnicodeDecodeError)
            self.assertRegex(str(cm.unraisable.exc_value),
                             "'utf-8' codec can't decode")

        for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES:
            self.assertIs(getitemstring(test_type(), b'a'), KeyError)
        # CRASHES getitemstring({}, NULL)
        # CRASHES getitemstring(NULL, b'a')

    def test_dict_getitemref(self):
        # Test PyDict_GetItemRef()
        getitem = _testcapi.dict_getitemref
        for dict_type in ANYDICT_TYPES:
            dct = dict_type({'a': 1, '\U0001f40d': 2})
            self.assertEqual(getitem(dct, 'a'), 1)
            self.assertIs(getitem(dct, 'b'), KeyError)
            self.assertEqual(getitem(dct, '\U0001f40d'), 2)

        self.assertRaises(TypeError, getitem, {}, [])  # unhashable
        for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES:
            self.assertRaises(SystemError, getitem, test_type(), 'a')
        # CRASHES getitem({}, NULL)
        # CRASHES getitem(NULL, 'a')

    def test_dict_getitemstringref(self):
        # Test PyDict_GetItemStringRef()
        getitemstring = _testcapi.dict_getitemstringref
        for dict_type in ANYDICT_TYPES:
            dct = dict_type({'a': 1, '\U0001f40d': 2})
            self.assertEqual(getitemstring(dct, b'a'), 1)
            self.assertIs(getitemstring(dct, b'b'), KeyError)
            self.assertEqual(getitemstring(dct, '\U0001f40d'.encode()), 2)

        self.assertRaises(UnicodeDecodeError, getitemstring, {}, INVALID_UTF8)
        for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES:
            self.assertRaises(SystemError, getitemstring, test_type(), b'a')
        # CRASHES getitemstring({}, NULL)
        # CRASHES getitemstring(NULL, b'a')

    def test_dict_getitemwitherror(self):
        # Test PyDict_GetItemWithError()
        getitem = _testlimitedcapi.dict_getitemwitherror
        for dict_type in ANYDICT_TYPES:
            dct = dict_type({'a': 1, '\U0001f40d': 2})
            self.assertEqual(getitem(dct, 'a'), 1)
            self.assertIs(getitem(dct, 'b'), KeyError)
            self.assertEqual(getitem(dct, '\U0001f40d'), 2)

        self.assertRaises(TypeError, getitem, {}, [])  # unhashable
        for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES:
            self.assertRaises(SystemError, getitem, [], 'a')
        # CRASHES getitem({}, NULL)
        # CRASHES getitem(NULL, 'a')

    def test_dict_contains(self):
        # Test PyDict_Contains()
        contains = _testlimitedcapi.dict_contains
        for dict_type in ANYDICT_TYPES:
            dct = dict_type({'a': 1, '\U0001f40d': 2})
            self.assertTrue(contains(dct, 'a'))
            self.assertFalse(contains(dct, 'b'))
            self.assertTrue(contains(dct, '\U0001f40d'))

        self.assertRaises(TypeError, contains, {}, [])  # unhashable
        for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES:
            self.assertRaises(SystemError, contains, test_type(), 'a')

        # CRASHES contains({}, NULL)
        # CRASHES contains(NULL, 'a')

    def test_dict_contains_string(self):
        # Test PyDict_ContainsString()
        contains_string = _testcapi.dict_containsstring
        for dict_type in ANYDICT_TYPES:
            dct = dict_type({'a': 1, '\U0001f40d': 2})
            self.assertTrue(contains_string(dct, b'a'))
            self.assertFalse(contains_string(dct, b'b'))
            self.assertTrue(contains_string(dct, '\U0001f40d'.encode()))
            self.assertRaises(UnicodeDecodeError, contains_string, dct, INVALID_UTF8)

        for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES:
            self.assertRaises(SystemError, contains_string, test_type(), b'a')

        # CRASHES contains({}, NULL)
        # CRASHES contains(NULL, b'a')

    @contextlib.contextmanager
    def frozendict_does_not_support(self, what):
        errmsg = f'frozendict object does not support item {what}'
        with self.assertRaisesRegex(TypeError, errmsg):
            yield

    def test_dict_setitem(self):
        # Test PyDict_SetItem()
        setitem = _testlimitedcapi.dict_setitem
        for dict_type in DICT_TYPES:
            dct = dict_type()
            setitem(dct, 'a', 5)
            self.assertEqual(dct, {'a': 5})
            setitem(dct, '\U0001f40d', 8)
            self.assertEqual(dct, {'a': 5, '\U0001f40d': 8})

        self.assertRaises(TypeError, setitem, {}, [], 5)  # unhashable
        for test_type in FROZENDICT_TYPES:
            with self.frozendict_does_not_support('assignment'):
                setitem(test_type(), 'a', 5)
        for test_type in MAPPING_TYPES + OTHER_TYPES:
            self.assertRaises(SystemError, setitem, test_type(), 'a', 5)
        # CRASHES setitem({}, NULL, 5)
        # CRASHES setitem({}, 'a', NULL)
        # CRASHES setitem(NULL, 'a', 5)

    def test_dict_setitemstring(self):
        # Test PyDict_SetItemString()
        setitemstring = _testlimitedcapi.dict_setitemstring
        for dict_type in DICT_TYPES:
            dct = dict_type()
            setitemstring(dct, b'a', 5)
            self.assertEqual(dct, {'a': 5})
            setitemstring(dct, '\U0001f40d'.encode(), 8)
            self.assertEqual(dct, {'a': 5, '\U0001f40d': 8})

        self.assertRaises(UnicodeDecodeError, setitemstring, {}, INVALID_UTF8, 5)
        for test_type in FROZENDICT_TYPES:
            with self.frozendict_does_not_support('assignment'):
                setitemstring(test_type(), b'a', 5)
        for test_type in MAPPING_TYPES + OTHER_TYPES:
            self.assertRaises(SystemError, setitemstring, test_type(), b'a', 5)
        # CRASHES setitemstring({}, NULL, 5)
        # CRASHES setitemstring({}, b'a', NULL)
        # CRASHES setitemstring(NULL, b'a', 5)

    def test_dict_delitem(self):
        # Test PyDict_DelItem()
        delitem = _testlimitedcapi.dict_delitem
        for dict_type in DICT_TYPES:
            dct = dict_type({'a': 1, 'c': 2, '\U0001f40d': 3})
            delitem(dct, 'a')
            self.assertEqual(dct, {'c': 2, '\U0001f40d': 3})
            self.assertRaises(KeyError, delitem, dct, 'b')
            delitem(dct, '\U0001f40d')
            self.assertEqual(dct, {'c': 2})

        self.assertRaises(TypeError, delitem, {}, [])  # unhashable
        for test_type in FROZENDICT_TYPES:
            with self.frozendict_does_not_support('deletion'):
                delitem(test_type({'a': 1}), 'a')
        for test_type in MAPPING_TYPES:
            self.assertRaises(SystemError, delitem, test_type({'a': 1}), 'a')
        for test_type in OTHER_TYPES:
            self.assertRaises(SystemError, delitem, test_type(), 'a')
        # CRASHES delitem({}, NULL)
        # CRASHES delitem(NULL, 'a')

    def test_dict_delitemstring(self):
        # Test PyDict_DelItemString()
        delitemstring = _testlimitedcapi.dict_delitemstring
        for dict_type in DICT_TYPES:
            dct = dict_type({'a': 1, 'c': 2, '\U0001f40d': 3})
            delitemstring(dct, b'a')
            self.assertEqual(dct, {'c': 2, '\U0001f40d': 3})
            self.assertRaises(KeyError, delitemstring, dct, b'b')
            delitemstring(dct, '\U0001f40d'.encode())
            self.assertEqual(dct, {'c': 2})

        self.assertRaises(UnicodeDecodeError, delitemstring, {}, INVALID_UTF8)
        for test_type in FROZENDICT_TYPES:
            with self.frozendict_does_not_support('deletion'):
                delitemstring(test_type({'a': 1}), b'a')
        for test_type in MAPPING_TYPES:
            self.assertRaises(SystemError, delitemstring, test_type({'a': 1}), b'a')
        for test_type in OTHER_TYPES:
            self.assertRaises(SystemError, delitemstring, test_type(), b'a')
        # CRASHES delitemstring({}, NULL)
        # CRASHES delitemstring(NULL, b'a')

    def test_dict_setdefault(self):
        # Test PyDict_SetDefault()
        setdefault = _testcapi.dict_setdefault
        for dict_type in DICT_TYPES:
            dct = dict_type()
            self.assertEqual(setdefault(dct, 'a', 5), 5)
            self.assertEqual(dct, {'a': 5})
            self.assertEqual(setdefault(dct, 'a', 8), 5)
            self.assertEqual(dct, {'a': 5})

        self.assertRaises(TypeError, setdefault, {}, [], 5)  # unhashable
        for test_type in FROZENDICT_TYPES:
            with self.frozendict_does_not_support('assignment'):
                setdefault(test_type(), 'a', 5)
        for test_type in MAPPING_TYPES + OTHER_TYPES:
            self.assertRaises(SystemError, setdefault, test_type(), 'a', 5)
        # CRASHES setdefault({}, NULL, 5)
        # CRASHES setdefault({}, 'a', NULL)
        # CRASHES setdefault(NULL, 'a', 5)

    def test_dict_setdefaultref(self):
        # Test PyDict_SetDefaultRef()
        setdefault = _testcapi.dict_setdefaultref
        for dict_type in DICT_TYPES:
            dct = dict_type()
            self.assertEqual(setdefault(dct, 'a', 5), 5)
            self.assertEqual(dct, {'a': 5})
            self.assertEqual(setdefault(dct, 'a', 8), 5)
            self.assertEqual(dct, {'a': 5})

        self.assertRaises(TypeError, setdefault, {}, [], 5)  # unhashable
        for test_type in FROZENDICT_TYPES:
            with self.frozendict_does_not_support('assignment'):
                setdefault(test_type(), 'a', 5)
        for test_type in MAPPING_TYPES + OTHER_TYPES:
            self.assertRaises(SystemError, setdefault, test_type(), 'a', 5)
        # CRASHES setdefault({}, NULL, 5)
        # CRASHES setdefault({}, 'a', NULL)
        # CRASHES setdefault(NULL, 'a', 5)

    def test_mapping_keys_valuesitems(self):
        # Test PyDict_Keys(), PyDict_Values() and PyDict_Items()
        class BadMapping(dict):
            def keys(self):
                return None
            def values(self):
                return None
            def items(self):
                return None
        dict_obj = {'foo': 1, 'bar': 2, 'spam': 3}
        for dict_type in ANYDICT_TYPES + (BadMapping,):
            mapping = dict_type(dict_obj)
            self.assertListEqual(_testlimitedcapi.dict_keys(mapping),
                                 list(dict_obj.keys()))
            self.assertListEqual(_testlimitedcapi.dict_values(mapping),
                                 list(dict_obj.values()))
            self.assertListEqual(_testlimitedcapi.dict_items(mapping),
                                 list(dict_obj.items()))

        for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES:
            mapping = test_type()
            self.assertRaises(SystemError, _testlimitedcapi.dict_keys, mapping)
            self.assertRaises(SystemError, _testlimitedcapi.dict_values, mapping)
            self.assertRaises(SystemError, _testlimitedcapi.dict_items, mapping)

    def test_dict_next(self):
        # Test PyDict_Next()
        dict_next = _testlimitedcapi.dict_next
        self.assertIsNone(dict_next({}, 0))
        for dict_type in ANYDICT_TYPES:
            dct = dict_type({'a': 1, 'b': 2, 'c': 3})
            pos = 0
            pairs = []
            while True:
                res = dict_next(dct, pos)
                if res is None:
                    break
                rc, pos, key, value = res
                self.assertEqual(rc, 1)
                pairs.append((key, value))
            self.assertEqual(pairs, list(dct.items()))

        for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES:
            dct = test_type()
            pos = 0
            self.assertEqual(dict_next(dct, pos), None)

        # CRASHES dict_next(NULL, 0)

    def test_dict_update(self):
        # Test PyDict_Update()
        update = _testlimitedcapi.dict_update
        for cls1 in DICT_TYPES:
            for cls2 in ANYDICT_TYPES + MAPPING_TYPES:
                dct = cls1({'a': 1, 'b': 2})
                update(dct, cls2({'b': 3, 'c': 4}))
                self.assertEqual(dct, {'a': 1, 'b': 3, 'c': 4})

        self.assertRaises(AttributeError, update, {}, [])
        self.assertRaises(AttributeError, update, {}, 42)
        for test_type in FROZENDICT_TYPES:
            with self.frozendict_does_not_support('assignment'):
                update(test_type(), {})
        for test_type in MAPPING_TYPES + OTHER_TYPES:
            self.assertRaises(SystemError, update, test_type(), {})
        self.assertRaises(SystemError, update, {}, NULL)
        self.assertRaises(SystemError, update, NULL, {})

    def test_dict_merge(self):
        # Test PyDict_Merge()
        merge = _testlimitedcapi.dict_merge
        for cls1 in DICT_TYPES:
            for cls2 in ANYDICT_TYPES + MAPPING_TYPES:
                dct = cls1({'a': 1, 'b': 2})
                merge(dct, cls2({'b': 3, 'c': 4}), 0)
                self.assertEqual(dct, {'a': 1, 'b': 2, 'c': 4})
                dct = cls1({'a': 1, 'b': 2})
                merge(dct, cls2({'b': 3, 'c': 4}), 1)
                self.assertEqual(dct, {'a': 1, 'b': 3, 'c': 4})

        self.assertRaises(AttributeError, merge, {}, [], 0)
        self.assertRaises(AttributeError, merge, {}, 42, 0)
        for test_type in FROZENDICT_TYPES:
            with self.frozendict_does_not_support('assignment'):
                merge(test_type(), {}, 0)
        for test_type in MAPPING_TYPES + OTHER_TYPES:
            self.assertRaises(SystemError, merge, test_type(), {}, 0)
        self.assertRaises(SystemError, merge, {}, NULL, 0)
        self.assertRaises(SystemError, merge, NULL, {}, 0)

    def test_dict_mergefromseq2(self):
        # Test PyDict_MergeFromSeq2()
        mergefromseq2 = _testlimitedcapi.dict_mergefromseq2
        for cls1 in DICT_TYPES:
            for cls2 in list, iter:
                dct = cls1({'a': 1, 'b': 2})
                mergefromseq2(dct, cls2([('b', 3), ('c', 4)]), 0)
                self.assertEqual(dct, {'a': 1, 'b': 2, 'c': 4})
                dct = cls1({'a': 1, 'b': 2})
                mergefromseq2(dct, cls2([('b', 3), ('c', 4)]), 1)
                self.assertEqual(dct, {'a': 1, 'b': 3, 'c': 4})

        self.assertRaises(ValueError, mergefromseq2, {}, [(1,)], 0)
        self.assertRaises(ValueError, mergefromseq2, {}, [(1, 2, 3)], 0)
        self.assertRaises(TypeError, mergefromseq2, {}, [1], 0)
        self.assertRaises(TypeError, mergefromseq2, {}, 42, 0)
        for test_type in FROZENDICT_TYPES:
            with self.frozendict_does_not_support('assignment'):
                mergefromseq2(test_type(), [], 0)
        for test_type in MAPPING_TYPES + OTHER_TYPES:
            self.assertRaises(SystemError, mergefromseq2, test_type(), [], 0)
        # CRASHES mergefromseq2({}, NULL, 0)
        # CRASHES mergefromseq2(NULL, {}, 0)

    def test_dict_pop(self):
        # Test PyDict_Pop()
        dict_pop = _testcapi.dict_pop
        dict_pop_null = _testcapi.dict_pop_null

        for dict_type in DICT_TYPES:
            # key present, get removed value
            mydict = dict_type({"key": "value", "key2": "value2"})
            self.assertEqual(dict_pop(mydict, "key"), (1, "value"))
            self.assertEqual(mydict, {"key2": "value2"})
            self.assertEqual(dict_pop(mydict, "key2"), (1, "value2"))
            self.assertEqual(mydict, {})

            # key present, ignore removed value
            mydict = dict_type({"key": "value", "key2": "value2"})
            self.assertEqual(dict_pop_null(mydict, "key"), 1)
            self.assertEqual(mydict, {"key2": "value2"})
            self.assertEqual(dict_pop_null(mydict, "key2"), 1)
            self.assertEqual(mydict, {})

            # key missing, expect removed value; empty dict has a fast path
            mydict = dict_type()
            self.assertEqual(dict_pop(mydict, "key"), (0, NULL))
            mydict = dict_type({"a": 1})
            self.assertEqual(dict_pop(mydict, "key"), (0, NULL))

            # key missing, ignored removed value; empty dict has a fast path
            mydict = dict_type()
            self.assertEqual(dict_pop_null(mydict, "key"), 0)
            mydict = dict_type({"a": 1})
            self.assertEqual(dict_pop_null(mydict, "key"), 0)

            # key error; don't hash key if dict is empty
            not_hashable_key = ["list"]
            mydict = dict_type()
            self.assertEqual(dict_pop(mydict, not_hashable_key), (0, NULL))
            dict_pop(mydict, NULL)  # key is not checked if dict is empty
            mydict = dict_type({'key': 1})
            with self.assertRaises(TypeError):
                dict_pop(mydict, not_hashable_key)

        # wrong dict type
        for test_type in FROZENDICT_TYPES:
            with self.frozendict_does_not_support('deletion'):
                dict_pop(test_type(), "key")
        for test_type in MAPPING_TYPES + OTHER_TYPES:
            not_dict = test_type()
            self.assertRaises(SystemError, dict_pop, not_dict, "key")
            self.assertRaises(SystemError, dict_pop_null, not_dict, "key")

        # CRASHES dict_pop(NULL, "key")
        # CRASHES dict_pop({"a": 1}, NULL)

    def test_dict_popstring(self):
        # Test PyDict_PopString()
        dict_popstring = _testcapi.dict_popstring
        dict_popstring_null = _testcapi.dict_popstring_null

        for dict_type in DICT_TYPES:
            # key present, get removed value
            mydict = dict_type({"key": "value", "key2": "value2"})
            self.assertEqual(dict_popstring(mydict, "key"), (1, "value"))
            self.assertEqual(mydict, {"key2": "value2"})
            self.assertEqual(dict_popstring(mydict, "key2"), (1, "value2"))
            self.assertEqual(mydict, {})

            # key present, ignore removed value
            mydict = dict_type({"key": "value", "key2": "value2"})
            self.assertEqual(dict_popstring_null(mydict, "key"), 1)
            self.assertEqual(mydict, {"key2": "value2"})
            self.assertEqual(dict_popstring_null(mydict, "key2"), 1)
            self.assertEqual(mydict, {})

            # key missing; empty dict has a fast path
            mydict = dict_type()
            self.assertEqual(dict_popstring(mydict, "key"), (0, NULL))
            self.assertEqual(dict_popstring_null(mydict, "key"), 0)
            mydict = dict_type({"a": 1})
            self.assertEqual(dict_popstring(mydict, "key"), (0, NULL))
            self.assertEqual(dict_popstring_null(mydict, "key"), 0)

            # non-ASCII key
            non_ascii = '\U0001f40d'
            dct = dict_type({'\U0001f40d': 123})
            self.assertEqual(dict_popstring(dct, '\U0001f40d'.encode()), (1, 123))
            dct = dict_type({'\U0001f40d': 123})
            self.assertEqual(dict_popstring_null(dct, '\U0001f40d'.encode()), 1)

            # key error
            mydict = dict_type({1: 2})
            self.assertRaises(UnicodeDecodeError, dict_popstring, mydict, INVALID_UTF8)
            self.assertRaises(UnicodeDecodeError, dict_popstring_null, mydict, INVALID_UTF8)

        # wrong dict type
        for test_type in FROZENDICT_TYPES:
            with self.frozendict_does_not_support('deletion'):
                dict_popstring(test_type(), "key")
        for test_type in MAPPING_TYPES + OTHER_TYPES:
            not_dict = test_type()
            self.assertRaises(SystemError, dict_popstring, not_dict, "key")
            self.assertRaises(SystemError, dict_popstring_null, not_dict, "key")

        # CRASHES dict_popstring(NULL, "key")
        # CRASHES dict_popstring({}, NULL)
        # CRASHES dict_popstring({"a": 1}, NULL)

    def test_frozendict_check(self):
        # Test PyFrozenDict_Check()
        check = _testcapi.frozendict_check
        for dict_type in FROZENDICT_TYPES:
            self.assertTrue(check(dict_type(x=1)))
        for dict_type in NOT_FROZENDICT_TYPES + OTHER_TYPES:
            self.assertFalse(check(dict_type()))
        # CRASHES check(NULL)

    def test_frozendict_checkexact(self):
        # Test PyFrozenDict_CheckExact()
        check = _testcapi.frozendict_checkexact
        for dict_type in FROZENDICT_TYPES:
            self.assertEqual(check(dict_type(x=1)), dict_type == frozendict)
        for dict_type in NOT_FROZENDICT_TYPES + OTHER_TYPES:
            self.assertFalse(check(dict_type()))
        # CRASHES check(NULL)

    def test_anydict_check(self):
        # Test PyAnyDict_Check()
        check = _testcapi.anydict_check
        for dict_type in ANYDICT_TYPES:
            self.assertTrue(check(dict_type({1: 2})))
        for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES:
            self.assertFalse(check(test_type()))
        # CRASHES check(NULL)

    def test_anydict_checkexact(self):
        # Test PyAnyDict_CheckExact()
        check = _testcapi.anydict_checkexact
        for dict_type in ANYDICT_TYPES:
            self.assertEqual(check(dict_type(x=1)),
                             dict_type in (dict, frozendict))
        for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES:
            self.assertFalse(check(test_type()))
        # CRASHES check(NULL)

    def test_frozendict_new(self):
        # Test PyFrozenDict_New()
        frozendict_new = _testcapi.frozendict_new

        for dict_type in ANYDICT_TYPES:
            dct = frozendict_new(dict_type({'x': 1}))
            self.assertEqual(dct, frozendict(x=1))
            self.assertIs(type(dct), frozendict)

        dct = frozendict_new([('x', 1), ('y', 2)])
        self.assertEqual(dct, frozendict(x=1, y=2))
        self.assertIs(type(dct), frozendict)

        # PyFrozenDict_New(frozendict) returns the same object unmodified
        fd = frozendict(a=1, b=2, c=3)
        fd2 = frozendict_new(fd)
        self.assertIs(fd2, fd)

        fd = FrozenDictSubclass(a=1, b=2, c=3)
        fd2 = frozendict_new(fd)
        self.assertIsNot(fd2, fd)
        self.assertEqual(fd2, fd)

        # PyFrozenDict_New(NULL) creates an empty dictionary
        dct = frozendict_new(NULL)
        self.assertEqual(dct, frozendict())
        self.assertIs(type(dct), frozendict)


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