|
8 | 8 | from test.support.import_helper import import_fresh_module |
9 | 9 |
|
10 | 10 | import collections.abc |
11 | | -from collections import namedtuple, UserDict |
| 11 | +from collections import namedtuple, UserDict, OrderedDict |
12 | 12 | import copy |
13 | 13 | import _datetime |
14 | 14 | import gc |
@@ -1391,26 +1391,127 @@ def __hash__(self): |
1391 | 1391 | view = self.mappingproxy(mapping) |
1392 | 1392 | self.assertEqual(hash(view), hash(mapping)) |
1393 | 1393 |
|
1394 | | - def test_richcompare(self): |
1395 | | - mp1 = self.mappingproxy({'a': 1}) |
1396 | | - mp1_2 = self.mappingproxy({'a': 1}) |
1397 | | - mp2 = self.mappingproxy({'a': 2}) |
| 1394 | + def check_richcompare(self, mapping_type): |
| 1395 | + data1 = mapping_type({'a': 1}) |
| 1396 | + data2 = mapping_type({'a': 2}) |
| 1397 | + mp1 = self.mappingproxy(data1) |
| 1398 | + copy1 = self.mappingproxy(data1) |
| 1399 | + mp2 = self.mappingproxy(data2) |
| 1400 | + |
| 1401 | + self.assertTrue(mp1 == data1) |
| 1402 | + self.assertTrue(data1 == mp1) |
| 1403 | + self.assertTrue(copy1 == data1) |
| 1404 | + |
| 1405 | + self.assertTrue(mp1 == copy1) |
| 1406 | + self.assertFalse(mp1 != copy1) |
1398 | 1407 |
|
1399 | | - self.assertTrue(mp1 == mp1_2) |
1400 | | - self.assertFalse(mp1 != mp1_2) |
1401 | 1408 | self.assertFalse(mp1 == mp2) |
1402 | 1409 | self.assertTrue(mp1 != mp2) |
| 1410 | + self.assertFalse(mp2 == data1) |
| 1411 | + self.assertTrue(mp2 != data1) |
| 1412 | + self.assertTrue(data1 != mp2) |
1403 | 1413 |
|
1404 | 1414 | msg = "not supported between instances of 'mappingproxy' and 'mappingproxy'" |
1405 | | - |
1406 | 1415 | with self.assertRaisesRegex(TypeError, msg): |
1407 | 1416 | mp1 > mp2 |
1408 | 1417 | with self.assertRaisesRegex(TypeError, msg): |
1409 | | - mp1 < mp1_2 |
| 1418 | + mp1 < copy1 |
1410 | 1419 | with self.assertRaisesRegex(TypeError, msg): |
1411 | 1420 | mp2 >= mp2 |
1412 | 1421 | with self.assertRaisesRegex(TypeError, msg): |
1413 | | - mp1_2 <= mp1 |
| 1422 | + copy1 <= mp1 |
| 1423 | + |
| 1424 | + if mapping_type.__module__ == 'collections': |
| 1425 | + mapping_name = f'{mapping_type.__module__}.{mapping_type.__name__}' |
| 1426 | + else: |
| 1427 | + mapping_name = mapping_type.__name__ |
| 1428 | + with self.assertRaisesRegex( |
| 1429 | + TypeError, |
| 1430 | + f"not supported between instances of 'mappingproxy' and '{mapping_name}'", |
| 1431 | + ): |
| 1432 | + copy1 <= data1 |
| 1433 | + |
| 1434 | + class Evil: |
| 1435 | + def __eq__(self, other): |
| 1436 | + return other |
| 1437 | + |
| 1438 | + result = (mp1 == Evil()) |
| 1439 | + self.assertIs(type(result), mapping_type) |
| 1440 | + if mapping_type == dict: |
| 1441 | + # Evil.__eq__() gets a copy of the mapping |
| 1442 | + self.assertEqual(result, data1) |
| 1443 | + else: |
| 1444 | + self.assertIs(result, data1) |
| 1445 | + |
| 1446 | + def test_richcompare_dict(self): |
| 1447 | + self.check_richcompare(dict) |
| 1448 | + |
| 1449 | + def test_richcompare_custom_mapping(self): |
| 1450 | + class CustomMapping(collections.abc.Mapping): |
| 1451 | + def __init__(self, data): |
| 1452 | + self._data = data |
| 1453 | + |
| 1454 | + def __getitem__(self, key): |
| 1455 | + return self._data[key] |
| 1456 | + |
| 1457 | + def __iter__(self): |
| 1458 | + return iter(self._data) |
| 1459 | + |
| 1460 | + def __len__(self): |
| 1461 | + return len(self._data) |
| 1462 | + |
| 1463 | + def __contains__(self, item): |
| 1464 | + return item in self._data |
| 1465 | + |
| 1466 | + self.check_richcompare(CustomMapping) |
| 1467 | + |
| 1468 | + class CustomMapping2(CustomMapping): |
| 1469 | + def __eq__(self, other): |
| 1470 | + return ( |
| 1471 | + isinstance(other, CustomMapping) |
| 1472 | + and self._data == other._data |
| 1473 | + ) |
| 1474 | + |
| 1475 | + mp1 = self.mappingproxy(CustomMapping({'a': 1})) |
| 1476 | + self.assertEqual(mp1, CustomMapping2({'a': 1})) |
| 1477 | + self.assertNotEqual(CustomMapping2({'a': 1}), mp1) |
| 1478 | + |
| 1479 | + def test_richcompare_odict(self): |
| 1480 | + self.check_richcompare(OrderedDict) |
| 1481 | + |
| 1482 | + def test_richcompare_frozendict(self): |
| 1483 | + self.check_richcompare(frozendict) |
| 1484 | + |
| 1485 | + def test_richcompare_evil(self): |
| 1486 | + # This test used to mutate the list dictionary, |
| 1487 | + # but MappingProxyType now creates a copy to call `__eq__`: |
| 1488 | + # https://github.com/python/cpython/issues/152405 |
| 1489 | + key = "__mappingproxy_crash_key__" |
| 1490 | + |
| 1491 | + class Evil: |
| 1492 | + def __eq__(self, other): |
| 1493 | + other[key] = 1 |
| 1494 | + return other |
| 1495 | + |
| 1496 | + # Checks that it does not mutate the internals of `MappingProxyType`: |
| 1497 | + dc = {} |
| 1498 | + leaked = self.mappingproxy(dc) == Evil() |
| 1499 | + self.assertIs(type(leaked), dict) |
| 1500 | + self.assertIn(key, leaked) |
| 1501 | + self.assertNotIn(key, dc) |
| 1502 | + |
| 1503 | + # Exposes the internals of `MappingProxyType` via richcompare: |
| 1504 | + leaked = vars(list) == Evil() |
| 1505 | + self.assertIs(type(leaked), dict) |
| 1506 | + self.assertIn(key, leaked) |
| 1507 | + name = "__mappingproxy_crash_probe__" |
| 1508 | + leaked[name] = lambda self: "probe" |
| 1509 | + self.assertIn(name, leaked) |
| 1510 | + self.assertNotHasAttr(list, key) |
| 1511 | + self.assertNotHasAttr(list, name) # it used to return `True` |
| 1512 | + del leaked[name] |
| 1513 | + self.assertNotIn(name, leaked) |
| 1514 | + self.assertNotHasAttr(list, name) # it used to crash |
1414 | 1515 |
|
1415 | 1516 |
|
1416 | 1517 | class ClassCreationTests(unittest.TestCase): |
|
0 commit comments