https://github.com/python/cpython/commit/4036afb56a4c6ba97a2f1471961b97d8e257c416
commit: 4036afb56a4c6ba97a2f1471961b97d8e257c416
branch: main
author: sobolevn <[email protected]>
committer: ambv <[email protected]>
date: 2026-10-21T15:53:04-07:00
summary:

gh-94808: add tests covering `PySequence_[InPlace]Concat` (#99319)

Co-authored-by: Muse Code (Meta Muse Spark)
Co-authored-by: Lukasz Langa <[email protected]>

files:
M Lib/test/test_operator.py

diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py
index 68c8aadeb50823..4da4eeb242e01c 100644
--- a/Lib/test/test_operator.py
+++ b/Lib/test/test_operator.py
@@ -560,6 +560,221 @@ 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_subclasses(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):
+            pass
+
+        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.assertIs(type(res), list)
+        self.assertEqual(res, [1, 2, 'a', 'b'])
+        self.assertEqual(data1, ListSubclass([1, 2]))  # must not change
+        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(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.assertEqual(data1, TupleSubclass([1, 2]))  # must not change
+        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])
+        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, 3, 'a', 'b'])
+        self.assertEqual(data1, [1, 2, 3, '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):
+            pass
+
+        data1 = ListSubclass([1, 2])
+        data2 = ListSubclass(['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, 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(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.assertEqual(data1, TupleSubclass([1, 2]))  # must not change
+        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])
+        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_iconcat_without_getitem(self):
         operator = self.module
 

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to