|
| 1 | +# pyright: reportMissingImports=warning,reportAssertAlwaysTrue=none |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +import operator |
| 6 | +from typing import Any |
| 7 | +from collections.abc import Container |
| 8 | + |
| 9 | +import pytest |
| 10 | + |
| 11 | +import clr |
| 12 | +clr.AddReference('System.Collections.Immutable') |
| 13 | +from System import Nullable, Object, ValueType, Int32, String |
| 14 | +import System.Collections.Generic as Gen |
| 15 | +import System.Collections.Immutable as Imm |
| 16 | +import System.Collections.ObjectModel as OM |
| 17 | + |
| 18 | +kv_pairs_1 = ((0, "0"), (10, "10"), (20, "20"), (30, "30")) |
| 19 | +kv_pairs_2 = kv_pairs_1 + ((40, None), ) |
| 20 | +kv_pairs_3 = (("0", 0), ("10", 10), ("20", 20), ("30", 30)) |
| 21 | +kv_pairs_4 = kv_pairs_3 + (("40", None), ) |
| 22 | + |
| 23 | + |
| 24 | +def exactly_equal(val1, val2): |
| 25 | + return type(val1) is type(val2) and val1 == val2, '{0!r} != {1!r}'.format(val1, val2) |
| 26 | + |
| 27 | + |
| 28 | +def translate_pytype(pytype, *, nullable=False): |
| 29 | + if pytype is int: |
| 30 | + cstype = Int32 |
| 31 | + elif pytype is str: |
| 32 | + cstype = String |
| 33 | + else: |
| 34 | + raise NotImplementedError('Unsupported type: {0!r}'.format(pytype)) |
| 35 | + |
| 36 | + if nullable and issubclass(cstype, ValueType): |
| 37 | + cstype = Nullable[cstype] |
| 38 | + |
| 39 | + return cstype |
| 40 | + |
| 41 | + |
| 42 | +class MappingTests: |
| 43 | + ktype: type |
| 44 | + vtype: type |
| 45 | + nullable: bool |
| 46 | + cs_ktype: type |
| 47 | + cs_vtype: type |
| 48 | + mapped_to_null: Container[Any] |
| 49 | + dct: Any |
| 50 | + |
| 51 | + def __init_subclass__(cls, /, values=None, **kwargs): |
| 52 | + if values is not None: |
| 53 | + cls.ktype,cls.vtype,cls.nullable,cls.cs_ktype,cls.cs_vtype = cls.deduce_types(values) |
| 54 | + cls.mapped_to_null = tuple(key for key,val in values if val is None) |
| 55 | + |
| 56 | + @staticmethod |
| 57 | + def deduce_types(values): |
| 58 | + (ktype, ) = {type(key) for key,_ in values} |
| 59 | + vtypes = {type(val) for _,val in values} |
| 60 | + nullable = type(None) in vtypes |
| 61 | + if nullable: |
| 62 | + vtypes.remove(type(None)) |
| 63 | + (vtype, ) = vtypes |
| 64 | + cs_ktype = translate_pytype(ktype) |
| 65 | + cs_vtype = translate_pytype(vtype, nullable=nullable) |
| 66 | + return (ktype, vtype, nullable, cs_ktype, cs_vtype) |
| 67 | + |
| 68 | + def test_len(self): |
| 69 | + assert type(len(self.dct)) is int |
| 70 | + assert len(self.dct) == 4 if not self.nullable else 5 |
| 71 | + |
| 72 | + def test_iter(self): |
| 73 | + for idx,key in enumerate(self.dct): |
| 74 | + assert type(key) is self.ktype and int(key) == idx * 10 |
| 75 | + |
| 76 | + def test_keys(self): |
| 77 | + keys = sorted(self.dct.keys()) |
| 78 | +# print(f'### {list(self.dct.keys())}') # TODO: DELME |
| 79 | + for idx,key in enumerate(keys): |
| 80 | + assert exactly_equal(key, self.ktype(idx * 10)) |
| 81 | + |
| 82 | + def test_values(self): |
| 83 | + values = sorted(self.dct.values(), key=lambda val: val if val is not None else self.vtype(999)) |
| 84 | + for idx,val in enumerate(values): |
| 85 | + exp_val = None if self.ktype(10 * idx) in self.mapped_to_null else self.vtype(10 * idx) |
| 86 | + assert exactly_equal(val, exp_val) |
| 87 | + |
| 88 | + def test_items(self): |
| 89 | + items = sorted(self.dct.items(), key=operator.itemgetter(0)) |
| 90 | + for idx,tpl in enumerate(items): |
| 91 | + assert type(tpl) is tuple and len(tpl) == 2 |
| 92 | + key,val = tpl |
| 93 | + assert exactly_equal(key, self.ktype(idx * 10)) |
| 94 | + exp_val = None if self.ktype(10 * idx) in self.mapped_to_null else self.vtype(10 * idx) |
| 95 | + assert exactly_equal(val, exp_val) |
| 96 | + |
| 97 | + def test_contains(self): |
| 98 | + assert self.ktype(10) in self.dct |
| 99 | + assert self.ktype(50) not in self.dct |
| 100 | + assert 12.34 not in self.dct |
| 101 | + |
| 102 | + def test_getitem(self): |
| 103 | + for idx in range(len(self.dct)): |
| 104 | + val = self.dct[self.ktype(10 * idx)] |
| 105 | + assert exactly_equal(val, self.vtype(10 * idx)) |
| 106 | + |
| 107 | + def test_getitem_raise(self): |
| 108 | + with pytest.raises(KeyError): |
| 109 | + self.dct[self.ktype(50)] |
| 110 | + with pytest.raises(KeyError): |
| 111 | + self.dct[12.34] |
| 112 | + |
| 113 | + def test_get(self): |
| 114 | + val = self.dct.get(self.ktype(10)) |
| 115 | + assert exactly_equal(val, self.vtype(10)) |
| 116 | + assert self.dct.get(self.ktype(50)) is None |
| 117 | + val = self.dct.get(self.ktype(50), 123.1) |
| 118 | + assert val == 123.1 |
| 119 | + |
| 120 | + |
| 121 | +class MutableMappingTests(MappingTests): |
| 122 | + def get_copy(self) -> Any: |
| 123 | + raise NotImplementedError('must be overridden!') |
| 124 | + |
| 125 | + def test_setitem(self): |
| 126 | + dct = self.get_copy() |
| 127 | + key,val = (self.ktype(10), self.vtype(11)) |
| 128 | + dct[key] = val |
| 129 | + assert exactly_equal(dct[key], val) |
| 130 | + |
| 131 | + def test_setitem_raise(self): |
| 132 | + if isinstance(self.dct, Object): # this is only relevant for CLR types |
| 133 | + dct = self.get_copy() |
| 134 | + with pytest.raises(Exception): |
| 135 | + dct[12.34] = self.vtype(0) |
| 136 | + |
| 137 | + @pytest.mark.xfail(reason='Known to crash', run=False) |
| 138 | + def test_delitem(self): |
| 139 | + dct = self.get_copy() |
| 140 | + del dct[self.ktype(10)] |
| 141 | + assert self.ktype(10) not in dct |
| 142 | + |
| 143 | + @pytest.mark.xfail(reason='Known to crash', run=False) |
| 144 | + def test_delitem_raise(self): |
| 145 | + dct = self.get_copy() |
| 146 | + with pytest.raises(KeyError): |
| 147 | + del dct[12.34] |
| 148 | + |
| 149 | + def test_pop(self): |
| 150 | + dct = self.get_copy() |
| 151 | + length = len(dct) |
| 152 | + val = dct.pop(self.ktype(10)) |
| 153 | + assert exactly_equal(val, self.vtype(10)) |
| 154 | + val = dct.pop(self.ktype(10), self.vtype(11)) |
| 155 | + assert exactly_equal(val, self.vtype(11)) |
| 156 | + assert len(dct) == length - 1 |
| 157 | + |
| 158 | + def test_popitem(self): |
| 159 | + dct = self.get_copy() |
| 160 | + while len(dct) != 0: |
| 161 | + tpl = dct.popitem() |
| 162 | + assert type(tpl) is tuple and len(tpl) == 2 |
| 163 | + key,val = tpl |
| 164 | + assert type(key) is self.ktype |
| 165 | + assert type(val) is self.vtype or (self.nullable and val is None) |
| 166 | + if val is not None: |
| 167 | + assert int(key) == int(val) |
| 168 | + |
| 169 | + def test_clear(self): |
| 170 | + dct = self.get_copy() |
| 171 | + dct.clear() |
| 172 | + assert len(dct) == 0 |
| 173 | + assert dict(dct) == {} |
| 174 | + |
| 175 | + def test_setdefault(self): |
| 176 | + dct = self.get_copy() |
| 177 | + dct.setdefault(self.ktype(50), self.vtype(50)) |
| 178 | + assert exactly_equal(dct[self.ktype(50)], self.vtype(50)) |
| 179 | + |
| 180 | + def test_update(self): |
| 181 | + dct = self.get_copy() |
| 182 | + pydict = {self.ktype(num): self.vtype(num) for num in (30, 40)} |
| 183 | + if self.nullable: |
| 184 | + pydict[self.ktype(50)] = None |
| 185 | + dct.update(pydict) |
| 186 | + pydict.update({self.ktype(num): self.vtype(num) for num in (0, 10, 20)}) # put in the items we expect to be set already |
| 187 | + assert dict(dct) == pydict |
| 188 | + extra_vals = tuple((self.ktype(num), self.vtype(num)) for num in (60, 70)) |
| 189 | + dct.update(extra_vals) |
| 190 | + pydict.update(extra_vals) |
| 191 | + assert dict(dct) == pydict |
| 192 | + if self.ktype is str: |
| 193 | + dct.update(aaa=80, bbb=90) |
| 194 | + pydict.update(aaa=80, bbb=90) |
| 195 | + assert dict(dct) == pydict |
| 196 | + |
| 197 | + |
| 198 | +class PyDictTests(MutableMappingTests): |
| 199 | + def __init_subclass__(cls, /, values, **kwargs): |
| 200 | + super().__init_subclass__(values=values, **kwargs) |
| 201 | + cls.dct = dict(values) |
| 202 | + |
| 203 | + def get_copy(self): |
| 204 | + return self.dct.copy() |
| 205 | + |
| 206 | +# class TestPyDictIntStr (PyDictTests, values=kv_pairs_1): pass |
| 207 | +# class TestPyDictIntNullStr(PyDictTests, values=kv_pairs_2): pass |
| 208 | +# class TestPyDictStrInt (PyDictTests, values=kv_pairs_3): pass |
| 209 | +# class TestPyDictStrNullInt(PyDictTests, values=kv_pairs_4): pass |
| 210 | + |
| 211 | + |
| 212 | +def make_cs_dictionary(cs_ktype, cs_vtype, values): |
| 213 | + dct = Gen.Dictionary[cs_ktype, cs_vtype]() |
| 214 | + for key,val in values: |
| 215 | + dct[key] = None if val is None else val |
| 216 | + return dct |
| 217 | + |
| 218 | + |
| 219 | +class DictionaryTests(MutableMappingTests): |
| 220 | + def __init_subclass__(cls, /, values, **kwargs): |
| 221 | + super().__init_subclass__(values=values, **kwargs) |
| 222 | + cls.dct = make_cs_dictionary(cls.cs_ktype, cls.cs_vtype, values) |
| 223 | + |
| 224 | + def get_copy(self): |
| 225 | + return Gen.Dictionary[self.cs_ktype, self.cs_vtype](self.dct) |
| 226 | + |
| 227 | +class TestDictionaryIntStr (DictionaryTests, values=kv_pairs_1): pass |
| 228 | +class TestDictionaryIntNullStr(DictionaryTests, values=kv_pairs_2): pass |
| 229 | +class TestDictionaryStrInt (DictionaryTests, values=kv_pairs_3): pass |
| 230 | +class TestDictionaryStrNullInt(DictionaryTests, values=kv_pairs_4): pass |
| 231 | + |
| 232 | + |
| 233 | +class ReadOnlyDictionaryTests(MappingTests): |
| 234 | + def __init_subclass__(cls, /, values, **kwargs): |
| 235 | + super().__init_subclass__(values=values, **kwargs) |
| 236 | + dct = make_cs_dictionary(cls.cs_ktype, cls.cs_vtype, values) |
| 237 | + cls.dct = OM.ReadOnlyDictionary[cls.cs_ktype, cls.cs_vtype](dct) |
| 238 | + |
| 239 | +class ReadOnlyDictionaryIntStr (ReadOnlyDictionaryTests, values=kv_pairs_1): pass |
| 240 | +class ReadOnlyDictionaryIntNullStr(ReadOnlyDictionaryTests, values=kv_pairs_2): pass |
| 241 | +class ReadOnlyDictionaryStrInt (ReadOnlyDictionaryTests, values=kv_pairs_3): pass |
| 242 | +class ReadOnlyDictionaryStrNullInt(ReadOnlyDictionaryTests, values=kv_pairs_4): pass |
| 243 | + |
| 244 | + |
| 245 | +class ImmutableDictionaryTests(MappingTests): |
| 246 | + def __init_subclass__(cls, /, values, **kwargs): |
| 247 | + super().__init_subclass__(values=values, **kwargs) |
| 248 | + dct = make_cs_dictionary(cls.cs_ktype, cls.cs_vtype, values) |
| 249 | + cls.dct = Imm.ImmutableDictionary.ToImmutableDictionary[cls.cs_ktype, cls.cs_vtype](dct) |
| 250 | + |
| 251 | +class TestImmutableDictionaryIntStr (ImmutableDictionaryTests, values=kv_pairs_1): pass |
| 252 | +class TestImmutableDictionaryIntNullStr(ImmutableDictionaryTests, values=kv_pairs_2): pass |
| 253 | +class TestImmutableDictionaryStrInt (ImmutableDictionaryTests, values=kv_pairs_3): pass |
| 254 | +class TestImmutableDictionaryStrNullInt(ImmutableDictionaryTests, values=kv_pairs_4): pass |
0 commit comments