Tuples transformed to lists in server call. Bug?

I have this code at the client side:

  def button_test_click(self, **event_args):
    rc_dict = {'10': [(1, 1), (1, 2), (2, 2)], '11': [(1, 3), (2, 3)]}
    print("CLIENT rc-dict:", rc_dict)
    anvil.server.call('test', rc_dict) 

and this code at the server side:

@anvil.server.callable
def test(rc_dict):
  print("SERVER rc_dict:", rc_dict)

Which gives the following result:
CLIENT rc-dict: {‘10’: [(1, 1), (1, 2), (2, 2)], ‘11’: [(1, 3), (2, 3)]}
SERVER rc_dict: {‘10’: [[1, 1], [1, 2], [2, 2]], ‘11’: [[1, 3], [2, 3]]}

Apparantly the tuples changed into lists! What is happing here?

Anything sent between the client and server (in either direction) has to be serialised to JSON first. Anvil handles that for you automatically but, unfortunately, not everything in Python is available within JSON.

You’re seeing an example of that here. JSON has no equivalent of a tuple. The closest thing is a list, so that’s what Anvil uses to serialise the tuple for you.

1 Like

Thanks Owen, that at least clarifies it. The same problem occurs for the way back. However this delivers a not so helpfull text. ExecutionTerminatedError: Server code exited unexpectedly: followed by a hexadecimal code.
I finaly could debug this to the following: anvil.server.SerializationError: Cannot serialize return value from function. Cannot serialize dictionaries with keys that aren’t strings at msg[‘response’][0][0][(1, 1)].
Apparantly from Server to Client it doesn’t serialize to lists.

Well, that’s a slightly different (but related) issue…

It’s telling you that it can only serialise a dict if its keys are strings. In your first example, that’s the case, so the dict serialises just fine but with tuples converted to lists.

Here, it’s failing - presumably you have keys that are ints like 10 rather than the strings like '10' in your first example.

1 Like

Here’s the relevant section of the docs: https://anvil.works/docs/server#valid-arguments-and-return-values

1 Like

I think it would be a good feature request (if there’s not one already) for there to be a warning when serialization converts a tuple to a list.

Likewise a more helpful initial error for the case of non-serializable dictionaries, if possible.

2 Likes

I used the following two recursive functions to wrap the conversation between server and client in case tuples where exchanged. Perhaps usefull for others as well:

def string_tuples(inp):    # e.g. [(1, 4), (1, 5)] -> ['(1, 4)', '(1, 5)']
    # Every tuple (r, c) is stringed like '(r, c')', even if it is part of a (nested) list or dict.
    # In ANVIL the communication between server end client is transferred in JSON which has no representation for a tuple.
    # The opposite function is called "string_tuples(inp)".
    if type(inp) == tuple:
        return str(inp)
    elif type(inp) == list:
        return [string_tuples(element) for element in inp]
    elif type(inp) == dict:
        return {string_tuples(k):string_tuples(v) for k, v in inp.items()}
    else:
        return inp

def unstring_tuples(inp):    # e.g. ['(1, 4)', '(1, 5)'] -> [(1, 4), (1, 5)]
    # Every stringed tuple, like '(r, c)' is unstringed, even if it is part of a (nested) list or dict.
    # In ANVIL the communication between server end client is transferred in JSON which has no representation for a tuple.
    # The opposite function is called "unstring_tuples(inp)".
    if type(inp) == str:
        try:
            (r, c) = inp[1:-1].split(',')
            return tuple((int(r), int(c)))
        except:
            return inp
    elif type(inp) == list:
        return [unstring_tuples(element) for element in inp]
    elif type(inp) == dict:
        return {unstring_tuples(k): unstring_tuples(v) for k, v in inp.items()}
    else:
        return inp
1 Like

Another approach might be to subclass tuple with your own serialisation mechanism.

@anvil.server.portable_class
class tuple_(tuple):
    @classmethod
    def __new_deserialized__(cls, data, global_data):
        return cls(data)
    def __serialize__(self, global_data):
        return list(self)

And then replace something like

(1, 2) with tuple_(1, 2)

But you’ll still have the string problem for keys of dicts so maybe not.

1 Like