patch 9.1.1239: if_python: no tuple data type support

Problem:  if_python: no tuple data type support (after v9.1.1232)
Solution: Add support for using Vim tuple in the python interface
          (Yegappan Lakshmanan)

closes: #16964

Signed-off-by: Yegappan Lakshmanan <yegappan@yahoo.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/src/testdir/test_python3.vim b/src/testdir/test_python3.vim
index c044954..e59ddf9 100644
--- a/src/testdir/test_python3.vim
+++ b/src/testdir/test_python3.vim
@@ -8,6 +8,10 @@
   return [1]
 endfunction
 
+func Create_vim_tuple()
+  return ('a', 'b')
+endfunction
+
 func Create_vim_dict()
   return {'a': 1}
 endfunction
@@ -627,6 +631,107 @@
         \ 'Vim(py3):TypeError: index must be int or slice, not dict')
 endfunc
 
+" Test for the python Tuple object
+func Test_python3_tuple()
+  " Try to convert a null tuple
+  call AssertException(["py3 l = vim.eval('test_null_tuple()')"],
+        \ s:system_error_pat)
+
+  " Try to convert a Tuple with a null Tuple item
+  call AssertException(["py3 t = vim.eval('(test_null_tuple(),)')"],
+        \ s:system_error_pat)
+
+  " Try to convert a List with a null Tuple item
+  call AssertException(["py3 t = vim.eval('[test_null_tuple()]')"],
+        \ s:system_error_pat)
+
+  " Try to convert a Tuple with a null List item
+  call AssertException(["py3 t = vim.eval('(test_null_list(),)')"],
+        \ s:system_error_pat)
+
+  " Try to bind a null Tuple variable (works because an empty tuple is used)
+  let cmds =<< trim END
+    let t = test_null_tuple()
+    py3 tt = vim.bindeval('t')
+  END
+  call AssertException(cmds, '')
+
+  " Creating a tuple using different iterators
+  py3 t1 = vim.Tuple(['abc', 20, 1.2, (4, 5)])
+  call assert_equal(('abc', 20, 1.2, (4, 5)), py3eval('t1'))
+  py3 t2 = vim.Tuple('abc')
+  call assert_equal(('a', 'b', 'c'), py3eval('t2'))
+  py3 t3 = vim.Tuple({'color': 'red', 'model': 'ford'})
+  call assert_equal(('color', 'model'), py3eval('t3'))
+  py3 t4 = vim.Tuple()
+  call assert_equal((), py3eval('t4'))
+  py3 t5 = vim.Tuple(x**2 for x in range(5))
+  call assert_equal((0, 1, 4, 9, 16), py3eval('t5'))
+  py3 t6 = vim.Tuple(('abc', 20, 1.2, (4, 5)))
+  call assert_equal(('abc', 20, 1.2, (4, 5)), py3eval('t6'))
+
+  " Convert between Vim tuple/list and python tuple/list
+  py3 t = vim.Tuple(vim.bindeval("('a', ('b',), ['c'], {'s': 'd'})"))
+  call assert_equal(('a', ('b',), ['c'], {'s': 'd'}), py3eval('t'))
+  call assert_equal(['a', ('b',), ['c'], {'s': 'd'}], py3eval('list(t)'))
+  call assert_equal(('a', ('b',), ['c'], {'s': 'd'}), py3eval('tuple(t)'))
+
+  py3 l = vim.List(vim.bindeval("['e', ('f',), ['g'], {'s': 'h'}]"))
+  call assert_equal(('e', ('f',), ['g'], {'s': 'h'}), py3eval('tuple(l)'))
+
+  " Tuple assignment
+  py3 tt = vim.bindeval('("a", "b")')
+  call AssertException(['py3 tt[0] = 10'],
+        \ "Vim(py3):TypeError: 'vim.tuple' object does not support item assignment")
+  py3 tt = vim.bindeval('("a", "b")')
+  call AssertException(['py3 tt[0:1] = (10, 20)'],
+        \ "Vim(py3):TypeError: 'vim.tuple' object does not support item assignment")
+
+  " iterating over tuple from Python
+  py3 print([x for x in vim.bindeval("('a', 'b')")])
+
+  " modifying a list item within a tuple
+  let t = ('a', ['b', 'c'], 'd')
+  py3 vim.bindeval('t')[1][1] = 'x'
+  call assert_equal(('a', ['b', 'x'], 'd'), t)
+
+  " length of a tuple
+  let t = ()
+  py3 p_t = vim.bindeval('t')
+  call assert_equal(0, py3eval('len(p_t)'))
+  let t = ('a', )
+  py3 p_t = vim.bindeval('t')
+  call assert_equal(1, py3eval('len(p_t)'))
+  let t = ('a', 'b', 'c')
+  py3 p_t = vim.bindeval('t')
+  call assert_equal(3, py3eval('len(p_t)'))
+
+  " membership test
+  let t = ('a', 'b', 'c')
+  py3 p_t = vim.bindeval('t')
+  call assert_true(py3eval("b'c' in p_t"))
+  call assert_true(py3eval("b'd' not in p_t"))
+
+  py3 x = vim.eval('("a", (2), [3], {})')
+  call assert_equal(('a', '2', ['3'], {}), py3eval('x'))
+
+  " Using a keyword argument for a tuple
+  call AssertException(['py3 x = vim.Tuple(a=1)'],
+        \ 'Vim(py3):TypeError: tuple constructor does not accept keyword arguments')
+
+  " Using dict as an index
+  call AssertException(['py3 x = tt[{}]'],
+        \ 'Vim(py3):TypeError: index must be int or slice, not dict')
+  call AssertException(['py3 x = tt["abc"]'],
+        \ 'Vim(py3):TypeError: index must be int or slice, not str')
+
+  call AssertException(['py3 del tt.locked'],
+        \ 'Vim(py3):AttributeError: cannot delete vim.Tuple attributes')
+
+  call AssertException(['py3 tt.foobar = 1'],
+        \ 'Vim(py3):AttributeError: cannot set attribute foobar')
+endfunc
+
 " Test for the python Dict object
 func Test_python3_dict()
   " Try to convert a null Dict
@@ -1005,11 +1110,24 @@
         \ 'Vim(py3):TypeError: cannot modify fixed list')
 endfunc
 
+" Test for locking/unlocking a tuple
+func Test_tuple_lock()
+  let t = (1, 2, 3)
+  py3 t = vim.bindeval('t')
+  py3 t.locked = True
+  call assert_equal(1, islocked('t'))
+  py3 t.locked = False
+  call assert_equal(0, islocked('t'))
+endfunc
+
 " Test for py3eval()
 func Test_python3_pyeval()
   let l = py3eval('[0, 1, 2]')
   call assert_equal([0, 1, 2], l)
 
+  let t = py3eval('("a", "b", "c")')
+  call assert_equal(("a", "b", "c"), t)
+
   let d = py3eval('{"a": "b", "c": 1, "d": ["e"]}')
   call assert_equal([['a', 'b'], ['c', 1], ['d', ['e']]], sort(items(d)))
 
@@ -1036,12 +1154,14 @@
   let num = 0xbadb33f
   let d = {'a': 1, 'b': 2, 'c': str}
   let l = [ str, num, d ]
+  let t = ( str, num, d )
 
   let locals = #{
         \ s: str,
         \ n: num,
         \ d: d,
         \ l: l,
+        \ t: t,
         \ }
 
   " check basics
@@ -1049,9 +1169,11 @@
   call assert_equal(0xbadb33f, py3eval('n', locals))
   call assert_equal(d, py3eval('d', locals))
   call assert_equal(l, py3eval('l', locals))
+  call assert_equal(t, py3eval('t', locals))
   call assert_equal('a,b,c', py3eval('b",".join(l)', {'l': ['a', 'b', 'c']}))
   call assert_equal('hello', 's'->py3eval({'s': 'hello'}))
   call assert_equal('a,b,c', 'b",".join(l)'->py3eval({'l': ['a', 'b', 'c']}))
+  call assert_equal('a-b-c', 'b"-".join(t)'->py3eval({'t': ('a', 'b', 'c')}))
 
   py3 << trim EOF
   def __UpdateDict(d, upd):
@@ -1218,6 +1340,92 @@
         \ s:system_error_pat)
 endfunc
 
+" Slice
+func Test_python3_tuple_slice()
+  py3 tt = vim.bindeval('(0, 1, 2, 3, 4, 5)')
+  py3 t = tt[:4]
+  call assert_equal((0, 1, 2, 3), py3eval('t'))
+  py3 t = tt[2:]
+  call assert_equal((2, 3, 4, 5), py3eval('t'))
+  py3 t = tt[:-4]
+  call assert_equal((0, 1), py3eval('t'))
+  py3 t = tt[-2:]
+  call assert_equal((4, 5), py3eval('t'))
+  py3 t = tt[2:4]
+  call assert_equal((2, 3), py3eval('t'))
+  py3 t = tt[4:2]
+  call assert_equal((), py3eval('t'))
+  py3 t = tt[-4:-2]
+  call assert_equal((2, 3), py3eval('t'))
+  py3 t = tt[-2:-4]
+  call assert_equal((), py3eval('t'))
+  py3 t = tt[:]
+  call assert_equal((0, 1, 2, 3, 4, 5), py3eval('t'))
+  py3 t = tt[0:6]
+  call assert_equal((0, 1, 2, 3, 4, 5), py3eval('t'))
+  py3 t = tt[-10:10]
+  call assert_equal((0, 1, 2, 3, 4, 5), py3eval('t'))
+  py3 t = tt[4:2:-1]
+  call assert_equal((4, 3), py3eval('t'))
+  py3 t = tt[::2]
+  call assert_equal((0, 2, 4), py3eval('t'))
+  py3 t = tt[4:2:1]
+  call assert_equal((), py3eval('t'))
+
+  " Error case: Use an invalid index
+  call AssertException(['py3 x = tt[-10]'], 'Vim(py3):IndexError: tuple index out of range')
+
+  " Use a step value of 0
+  call AssertException(['py3 x = tt[0:3:0]'],
+        \ 'Vim(py3):ValueError: slice step cannot be zero')
+
+  " Error case: Invalid slice type
+  call AssertException(["py3 x = tt['abc']"],
+        \ "Vim(py3):TypeError: index must be int or slice, not str")
+
+  " Error case: List with a null tuple item
+  let t = (test_null_tuple(),)
+  py3 tt = vim.bindeval('t')
+  call AssertException(["py3 x = tt[:]"], s:system_error_pat)
+endfunc
+
+func Test_python3_pytuple_to_vimtuple()
+  let t = py3eval("('a', 'b')")
+  call assert_equal(('a', 'b'), t)
+  let t = py3eval("()")
+  call assert_equal((), t)
+  let t = py3eval("('x',)")
+  call assert_equal(('x',), t)
+  let t = py3eval("((1, 2), (), (3, 4))")
+  call assert_equal(((1, 2), (), (3, 4)), t)
+  let t = py3eval("((1, 2), {'a': 10}, [5, 6])")
+  call assert_equal(((1, 2), {'a': 10}, [5, 6]), t)
+
+  " Invalid python tuple
+  py3 << trim END
+    class FailingIter(object):
+      def __iter__(self):
+        raise NotImplementedError('iter')
+  END
+  call assert_fails('call py3eval("(1, FailingIter, 2)")',
+        \ 'E859: Failed to convert returned python object to a Vim value')
+
+  py3 del FailingIter
+endfunc
+
+" Test for tuple garbage collection
+func Test_python3_tuple_garbage_collect()
+  let t = (1, (2, 3), [4, 5], {'a': 6})
+  py3 py_t = vim.bindeval('t')
+  let save_testing = v:testing
+  let v:testing = 1
+  call test_garbagecollect_now()
+  let v:testing = save_testing
+
+  let new_t = py3eval('py_t')
+  call assert_equal((1, (2, 3), [4, 5], {'a': 6}), new_t)
+endfunc
+
 " Vars
 func Test_python3_vars()
   let g:foo = 'bac'
@@ -2189,6 +2397,7 @@
             ('range',      vim.current.range),
             ('dictionary', vim.bindeval('{}')),
             ('list',       vim.bindeval('[]')),
+            ('tuple',       vim.bindeval('()')),
             ('function',   vim.bindeval('function("tr")')),
             ('output',     sys.stdout),
         ):
@@ -2204,6 +2413,7 @@
     range:__dir__,append,end,start
     dictionary:__dir__,get,has_key,items,keys,locked,pop,popitem,scope,update,values
     list:__dir__,extend,locked
+    tuple:__dir__,locked
     function:__dir__,args,auto_rebind,self,softspace
     output:__dir__,close,closed,flush,isatty,readable,seekable,softspace,writable,write,writelines
   END
@@ -2216,7 +2426,9 @@
   call assert_equal({'a': 1}, py3eval('vim.Dictionary(a=1)'))
   call assert_equal({'a': 1}, py3eval('vim.Dictionary(((''a'', 1),))'))
   call assert_equal([], py3eval('vim.List()'))
+  call assert_equal((), py3eval('vim.Tuple()'))
   call assert_equal(['a', 'b', 'c', '7'], py3eval('vim.List(iter(''abc7''))'))
+  call assert_equal(('a', 'b', 'c', '7'), py3eval('vim.Tuple(iter(''abc7''))'))
   call assert_equal(function('tr'), py3eval('vim.Function(''tr'')'))
   call assert_equal(function('tr', [123, 3, 4]),
         \ py3eval('vim.Function(''tr'', args=[123, 3, 4])'))
@@ -4065,6 +4277,7 @@
 " Regression: Iterator for a Vim object should hold a reference.
 func Test_python3_iter_ref()
   let g:list_iter_ref_count_increase = -1
+  let g:tuple_iter_ref_count_increase = -1
   let g:dict_iter_ref_count_increase = -1
   let g:bufmap_iter_ref_count_increase = -1
   let g:options_iter_ref_count_increase = -1
@@ -4080,6 +4293,12 @@
       for el in v:
         vim.vars['list_iter_ref_count_increase'] = sys.getrefcount(v) - base_ref_count
 
+      create_tuple = vim.Function('Create_vim_tuple')
+      v = create_tuple()
+      base_ref_count = sys.getrefcount(v)
+      for el in v:
+        vim.vars['tuple_iter_ref_count_increase'] = sys.getrefcount(v) - base_ref_count
+
       create_dict = vim.Function('Create_vim_dict')
       v = create_dict()
       base_ref_count = sys.getrefcount(v)
@@ -4100,6 +4319,7 @@
   EOF
 
   call assert_equal(1, g:list_iter_ref_count_increase)
+  call assert_equal(1, g:tuple_iter_ref_count_increase)
   call assert_equal(1, g:dict_iter_ref_count_increase)
   if py3eval('sys.version_info[:2] < (3, 13)')
     call assert_equal(1, g:bufmap_iter_ref_count_increase)