From 3556649f7b06dd0f9a1af0f34c64d198a2b01842 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 10 Nov 2022 12:51:14 +0300 Subject: [PATCH 1/5] gh-94808: add tests covering `PySequence_[InPlace]Concat` --- Lib/test/test_operator.py | 145 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py index b7e38c23349878..df1067a521e0bc 100644 --- a/Lib/test/test_operator.py +++ b/Lib/test/test_operator.py @@ -505,6 +505,151 @@ def __getitem__(self, other): return 5 # so that C is a sequence self.assertEqual(operator.ixor (c, 5), "ixor") self.assertEqual(operator.iconcat (c, c), "iadd") + def test_concat(self): + operator = self.module + + # Simple cases: + data1 = [1, 2] + data2 = ['a', 'b'] + self.assertEqual(operator.concat(data1, data2), [1, 2, 'a', 'b']) + self.assertEqual(operator.concat(data1, data2), data1 + data2) + self.assertEqual(data1, [1, 2]) # must not change + self.assertEqual(data2, ['a', 'b']) # must not change + + data1 = (1, 2) + data2 = ('a', 'b') + self.assertEqual(operator.concat(data1, data2), (1, 2, 'a', 'b')) + self.assertEqual(operator.concat(data1, data2), data1 + data2) + self.assertEqual(data1, (1, 2)) # must not change + self.assertEqual(data2, ('a', 'b')) # must not change + + data1 = '12' + data2 = 'ab' + self.assertEqual(operator.concat(data1, data2), '12ab') + self.assertEqual(operator.concat(data1, data2), data1 + data2) + self.assertEqual(data1, '12') # must not change + self.assertEqual(data2, 'ab') # must not change + + # Subclasses: + class ListSubclass(list): ... + + data1 = ListSubclass([1, 2]) + data2 = ListSubclass(['a', 'b']) + self.assertEqual(operator.concat(data1, data2), + ListSubclass([1, 2, 'a', 'b'])) + self.assertEqual(operator.concat(data1, data2), data1 + data2) + self.assertEqual(data1, ListSubclass([1, 2])) # must not change + self.assertEqual(data2, ListSubclass(['a', 'b'])) # must not change + + # Custom type with `__add__`: + class TupleSubclass(tuple): + def __add__(self, other): + return other + self + + data1 = TupleSubclass([1, 2]) + data2 = ('a', 'b') + self.assertEqual(operator.concat(data1, data2), + TupleSubclass(['a', 'b', 1, 2])) + self.assertEqual(operator.concat(data1, data2), data1 + data2) + self.assertEqual(data1, TupleSubclass([1, 2])) # must not change + self.assertEqual(data2, ('a', 'b')) # must not change + + # Corner cases: + self.assertEqual(operator.concat([1, 2], []), [1, 2]) + self.assertEqual(operator.concat([], [1, 2]), [1, 2]) + self.assertEqual(operator.concat([], []), []) + + # Type errors: + self.assertRaises(TypeError, operator.concat, [], 1) + self.assertRaises(TypeError, operator.concat, 1, []) + self.assertRaises(TypeError, operator.concat, 1, 1) + self.assertRaises(TypeError, operator.concat, (), []) + self.assertRaises(TypeError, operator.concat, [], ()) + + # Returns `NotImplemented`: + class BrokenSeq(tuple): + def __add__(self, other): + return NotImplemented + + self.assertRaises(TypeError, operator.concat, BrokenSeq(), BrokenSeq()) + + def test_iconcat(self): + operator = self.module + + # Simple cases: + data1 = [1, 2,, 3] + data2 = ['a', 'b'] + res = operator.iconcat(data1, data2) + self.assertEqual(res, [1, 2, 'a', 'b']) + self.assertEqual(data1, [1, 2, 'a', 'b']) # must change + self.assertEqual(data2, ['a', 'b']) # must not change + + data1 = (1, 2) + data2 = ('a', 'b') + self.assertEqual(operator.iconcat(data1, data2), (1, 2, 'a', 'b')) + self.assertEqual(operator.iconcat(data1, data2), data1 + data2) + self.assertEqual(data1, (1, 2)) # must not change + self.assertEqual(data2, ('a', 'b')) # must not change + + data1 = '12' + data2 = 'ab' + self.assertEqual(operator.iconcat(data1, data2), '12ab') + self.assertEqual(operator.iconcat(data1, data2), data1 + data2) + self.assertEqual(data1, '12') # must not change + self.assertEqual(data2, 'ab') # must not change + + # Subclasses: + class ListSubclass(list): ... + + data1 = ListSubclass([1, 2]) + data2 = ListSubclass(['a', 'b']) + res = operator.iconcat(data1, data2) + self.assertEqual(res, ListSubclass([1, 2, 'a', 'b'])) + self.assertEqual(data1, ListSubclass([1, 2, 'a', 'b'])) # must change + self.assertEqual(data2, ListSubclass(['a', 'b'])) # must not change + + # Custom type with `__add__`: + class TupleSubclass(tuple): + def __add__(self, other): + return other + self + + data1 = TupleSubclass([1, 2]) + data2 = ('a', 'b') + self.assertEqual(operator.iconcat(data1, data2), + TupleSubclass(['a', 'b', 1, 2])) + self.assertEqual(operator.iconcat(data1, data2), data1 + data2) + self.assertEqual(data1, TupleSubclass([1, 2])) # must not change + self.assertEqual(data2, ('a', 'b')) # must not change + + # Corner cases: + self.assertEqual(operator.iconcat([1, 2], []), [1, 2]) + self.assertEqual(operator.iconcat([], [1, 2]), [1, 2]) + self.assertEqual(operator.iconcat([], []), []) + self.assertEqual(operator.iconcat([], ()), []) + + # Type errors: + self.assertRaises(TypeError, operator.iconcat, [], 1) + self.assertRaises(TypeError, operator.iconcat, 1, []) + self.assertRaises(TypeError, operator.iconcat, 1, 1) + self.assertRaises(TypeError, operator.iconcat, (), []) + + # Returns `NotImplemented`: + class BrokenSeq1(list): + def __add__(self, other): + return NotImplemented + def __iadd__(self, other): + return NotImplemented + + self.assertRaises(TypeError, + operator.iconcat, BrokenSeq1(), BrokenSeq1()) + + class BrokenSeq2(tuple): + def __add__(self, other): + return NotImplemented + + self.assertRaises(TypeError, + operator.iconcat, BrokenSeq2(), BrokenSeq2()) + def test_length_hint(self): operator = self.module class X(object): From 359b007b51f72277d740b691f0449908e53dda8d Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 10 Nov 2022 13:24:07 +0300 Subject: [PATCH 2/5] Typo --- Lib/test/test_operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py index df1067a521e0bc..cd98d1a7969c85 100644 --- a/Lib/test/test_operator.py +++ b/Lib/test/test_operator.py @@ -577,7 +577,7 @@ def test_iconcat(self): operator = self.module # Simple cases: - data1 = [1, 2,, 3] + data1 = [1, 2, 3] data2 = ['a', 'b'] res = operator.iconcat(data1, data2) self.assertEqual(res, [1, 2, 'a', 'b']) From 6cee271065f56b34caff7e4a00fc66558d14cabb Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 10 Nov 2022 13:59:13 +0300 Subject: [PATCH 3/5] Typo --- Lib/test/test_operator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py index cd98d1a7969c85..ed7467b6fce583 100644 --- a/Lib/test/test_operator.py +++ b/Lib/test/test_operator.py @@ -580,8 +580,8 @@ def test_iconcat(self): data1 = [1, 2, 3] data2 = ['a', 'b'] res = operator.iconcat(data1, data2) - self.assertEqual(res, [1, 2, 'a', 'b']) - self.assertEqual(data1, [1, 2, 'a', 'b']) # must change + self.assertEqual(res, [1, 2, 3, 'a', 'b']) + self.assertEqual(data1, [1, 2, 3, 'a', 'b']) # must change self.assertEqual(data2, ['a', 'b']) # must not change data1 = (1, 2) From 5893ba73b9f2eead7065b5ce4f9bffbb810d369f Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 25 Nov 2022 09:49:14 +0300 Subject: [PATCH 4/5] Address review --- Lib/test/test_operator.py | 40 +++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py index ed7467b6fce583..35e2c2c96b5652 100644 --- a/Lib/test/test_operator.py +++ b/Lib/test/test_operator.py @@ -531,27 +531,33 @@ def test_concat(self): self.assertEqual(data2, 'ab') # must not change # Subclasses: - class ListSubclass(list): ... + class ListSubclass(list): + pass data1 = ListSubclass([1, 2]) data2 = ListSubclass(['a', 'b']) - self.assertEqual(operator.concat(data1, data2), - ListSubclass([1, 2, 'a', 'b'])) - self.assertEqual(operator.concat(data1, data2), data1 + data2) + + res = operator.concat(data1, data2) + self.assertIsInstance(res, list) + self.assertEqual(res, [1, 2, 'a', 'b']) + self.assertIsInstance(data1, ListSubclass) self.assertEqual(data1, ListSubclass([1, 2])) # must not change + self.assertIsInstance(data2, ListSubclass) self.assertEqual(data2, ListSubclass(['a', 'b'])) # must not change # Custom type with `__add__`: class TupleSubclass(tuple): def __add__(self, other): - return other + self + return TupleSubclass(other + self) data1 = TupleSubclass([1, 2]) data2 = ('a', 'b') - self.assertEqual(operator.concat(data1, data2), - TupleSubclass(['a', 'b', 1, 2])) - self.assertEqual(operator.concat(data1, data2), data1 + data2) + res = operator.concat(data1, data2) + self.assertIsInstance(res, TupleSubclass) + self.assertEqual(res, TupleSubclass(['a', 'b', 1, 2])) + self.assertIsInstance(data1, TupleSubclass) self.assertEqual(data1, TupleSubclass([1, 2])) # must not change + self.assertIsInstance(data2, tuple) self.assertEqual(data2, ('a', 'b')) # must not change # Corner cases: @@ -599,26 +605,32 @@ def test_iconcat(self): self.assertEqual(data2, 'ab') # must not change # Subclasses: - class ListSubclass(list): ... + class ListSubclass(list): + pass data1 = ListSubclass([1, 2]) data2 = ListSubclass(['a', 'b']) res = operator.iconcat(data1, data2) - self.assertEqual(res, ListSubclass([1, 2, 'a', 'b'])) + self.assertIsInstance(res, ListSubclass) + self.assertEqual(res, [1, 2, 'a', 'b']) + self.assertIsInstance(data1, ListSubclass) self.assertEqual(data1, ListSubclass([1, 2, 'a', 'b'])) # must change + self.assertIsInstance(data2, ListSubclass) self.assertEqual(data2, ListSubclass(['a', 'b'])) # must not change # Custom type with `__add__`: class TupleSubclass(tuple): def __add__(self, other): - return other + self + return TupleSubclass(other + self) data1 = TupleSubclass([1, 2]) data2 = ('a', 'b') - self.assertEqual(operator.iconcat(data1, data2), - TupleSubclass(['a', 'b', 1, 2])) - self.assertEqual(operator.iconcat(data1, data2), data1 + data2) + res = operator.iconcat(data1, data2) + self.assertIsInstance(res, TupleSubclass) + self.assertEqual(res, TupleSubclass(['a', 'b', 1, 2])) + self.assertIsInstance(data1, TupleSubclass) self.assertEqual(data1, TupleSubclass([1, 2])) # must not change + self.assertIsInstance(data2, tuple) self.assertEqual(data2, ('a', 'b')) # must not change # Corner cases: From db068d79ecc151193ba5ad63c271ee2c869ade44 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 24 Mar 2023 15:18:41 +0300 Subject: [PATCH 5/5] Address review --- Lib/test/test_operator.py | 80 +++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py index fbff1b8173c23e..42a8a250dc2d16 100644 --- a/Lib/test/test_operator.py +++ b/Lib/test/test_operator.py @@ -539,30 +539,57 @@ class ListSubclass(list): data1 = ListSubclass([1, 2]) data2 = ListSubclass(['a', 'b']) + res = operator.concat(data1, data2) + self.assertIs(type(res), list) + self.assertEqual(res, [1, 2, 'a', 'b']) + self.assertEqual(data1, ListSubclass([1, 2])) # must not change + self.assertEqual(data2, ListSubclass(['a', 'b'])) # must not change + data1 = ListSubclass([1, 2]) + data2 = ['a', 'b'] res = operator.concat(data1, data2) - self.assertIsInstance(res, list) + self.assertIs(type(res), list) self.assertEqual(res, [1, 2, 'a', 'b']) - self.assertIsInstance(data1, ListSubclass) self.assertEqual(data1, ListSubclass([1, 2])) # must not change - self.assertIsInstance(data2, ListSubclass) + self.assertEqual(data2, ['a', 'b']) # must not change + + data1 = [1, 2] + data2 = ListSubclass(['a', 'b']) + res = operator.concat(data1, data2) + self.assertIs(type(res), list) + self.assertEqual(res, [1, 2, 'a', 'b']) + self.assertEqual(data1, [1, 2]) # must not change self.assertEqual(data2, ListSubclass(['a', 'b'])) # must not change # Custom type with `__add__`: class TupleSubclass(tuple): def __add__(self, other): - return TupleSubclass(other + self) + return TupleSubclass(list(other) + list(self)) + + data1 = TupleSubclass([1, 2]) + data2 = TupleSubclass(['a', 'b']) + res = operator.concat(data1, data2) + self.assertIsInstance(res, TupleSubclass) + self.assertEqual(res, TupleSubclass(['a', 'b', 1, 2])) + self.assertEqual(data1, TupleSubclass([1, 2])) # must not change + self.assertEqual(data2, TupleSubclass(['a', 'b'])) # must not change data1 = TupleSubclass([1, 2]) data2 = ('a', 'b') res = operator.concat(data1, data2) self.assertIsInstance(res, TupleSubclass) self.assertEqual(res, TupleSubclass(['a', 'b', 1, 2])) - self.assertIsInstance(data1, TupleSubclass) self.assertEqual(data1, TupleSubclass([1, 2])) # must not change - self.assertIsInstance(data2, tuple) self.assertEqual(data2, ('a', 'b')) # must not change + data1 = (1, 2) + data2 = TupleSubclass(['a', 'b']) + res = operator.concat(data1, data2) + self.assertIs(type(res), tuple) + self.assertEqual(res, (1, 2, 'a', 'b')) + self.assertEqual(data1, (1, 2)) # must not change + self.assertEqual(data2, TupleSubclass(['a', 'b'])) # must not change + # Corner cases: self.assertEqual(operator.concat([1, 2], []), [1, 2]) self.assertEqual(operator.concat([], [1, 2]), [1, 2]) @@ -615,27 +642,58 @@ class ListSubclass(list): data2 = ListSubclass(['a', 'b']) res = operator.iconcat(data1, data2) self.assertIsInstance(res, ListSubclass) - self.assertEqual(res, [1, 2, 'a', 'b']) + self.assertEqual(res, ListSubclass([1, 2, 'a', 'b'])) self.assertIsInstance(data1, ListSubclass) self.assertEqual(data1, ListSubclass([1, 2, 'a', 'b'])) # must change - self.assertIsInstance(data2, ListSubclass) + self.assertEqual(data2, ListSubclass(['a', 'b'])) # must not change + + data1 = ListSubclass([1, 2]) + data2 = ['a', 'b'] + res = operator.iconcat(data1, data2) + self.assertIsInstance(res, ListSubclass) + self.assertEqual(res, ListSubclass([1, 2, 'a', 'b'])) + self.assertIsInstance(data1, ListSubclass) + self.assertEqual(data1, ListSubclass([1, 2, 'a', 'b'])) # must change + self.assertEqual(data2, ['a', 'b']) # must not change + + data1 = [1, 2] + data2 = ListSubclass(['a', 'b']) + res = operator.iconcat(data1, data2) + self.assertIs(type(res), list) + self.assertEqual(res, [1, 2, 'a', 'b']) + self.assertIs(type(data1), list) + self.assertEqual(data1, [1, 2, 'a', 'b']) # must change self.assertEqual(data2, ListSubclass(['a', 'b'])) # must not change # Custom type with `__add__`: class TupleSubclass(tuple): def __add__(self, other): - return TupleSubclass(other + self) + return TupleSubclass(list(other) + list(self)) + + data1 = TupleSubclass([1, 2]) + data2 = TupleSubclass(['a', 'b']) + res = operator.iconcat(data1, data2) + self.assertIsInstance(res, TupleSubclass) + self.assertEqual(res, TupleSubclass(['a', 'b', 1, 2])) + self.assertEqual(data1, TupleSubclass([1, 2])) # must not change + self.assertEqual(data2, TupleSubclass(['a', 'b'])) # must not change data1 = TupleSubclass([1, 2]) data2 = ('a', 'b') res = operator.iconcat(data1, data2) self.assertIsInstance(res, TupleSubclass) self.assertEqual(res, TupleSubclass(['a', 'b', 1, 2])) - self.assertIsInstance(data1, TupleSubclass) self.assertEqual(data1, TupleSubclass([1, 2])) # must not change - self.assertIsInstance(data2, tuple) self.assertEqual(data2, ('a', 'b')) # must not change + data1 = (1, 2) + data2 = TupleSubclass(['a', 'b']) + res = operator.iconcat(data1, data2) + self.assertIs(type(res), tuple) + self.assertEqual(res, TupleSubclass([1, 2, 'a', 'b'])) + self.assertEqual(data1, (1, 2)) # must not change + self.assertEqual(data2, TupleSubclass(['a', 'b'])) # must not change + # Corner cases: self.assertEqual(operator.iconcat([1, 2], []), [1, 2]) self.assertEqual(operator.iconcat([], [1, 2]), [1, 2])