jupyter-widgets / ipydatagrid

Fast Datagrid widget for the Jupyter Notebook and JupyterLab
BSD 3-Clause "New" or "Revised" License
579 stars 51 forks source link

`jupyter_client` >= 7.0.0 breaks some `ipydatagrid` functionality #273

Closed ibdafna closed 3 years ago

ibdafna commented 3 years ago

This issue surfaced during routine CI tests in #272.

Moreover, jupyter_client >= 7.0.0 works fine with Python 3.6, but not >= 3.7. Specifically, it seems like grids with nested columns trigger this issue. The following snippet causes an exception:

import ipydatagrid as ipg
import pandas as pd
import numpy as np

# Columns
col_top_level = [
    "VeryLongValueFactors",
    "VeryLongValueFactors",
    "Even Longer Momentum Factors",
    "Even Longer Momentum Factors",
]
col_bottom_level = ["Factor_A", "Factor_B", "Factor_C", "Factor_D"]

# Rows
row_top_level = ["Sector 1", "Sector 1", "Sector 2", "Sector 2"]
row_bottom_level = ["Security A", "Security B", "Security C", "Security D"]

nested_df = pd.DataFrame(
    np.random.randn(4, 4).round(4),
    columns=pd.MultiIndex.from_arrays([col_top_level, col_bottom_level]),
    index=pd.MultiIndex.from_arrays(
        [row_top_level, row_bottom_level], names=("Sector", "Ticker")
    ),
)

nested_grid = ipg.DataGrid(
    nested_df,
)

nested_grid
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_9865/4137114305.py in <module>
     26 
     27 nested_grid = ipg.DataGrid(
---> 28     nested_df,
     29 )
     30 

~/dev/ipydatagrid/ipydatagrid/datagrid.py in __init__(self, dataframe, **kwargs)
    356         self.data = dataframe
    357         super().__init__(**kwargs)
--> 358         self._cell_click_handlers = CallbackDispatcher()
    359         self._cell_change_handlers = CallbackDispatcher()
    360         self.on_msg(self.__handle_custom_msg)

~/miniconda3/envs/ipydatagrid/lib/python3.7/site-packages/ipywidgets/widgets/widget.py in __init__(self, **kwargs)
    413 
    414         Widget._call_widget_constructed(self)
--> 415         self.open()
    416 
    417     def __del__(self):

~/miniconda3/envs/ipydatagrid/lib/python3.7/site-packages/ipywidgets/widgets/widget.py in open(self)
    436                 args['comm_id'] = self._model_id
    437 
--> 438             self.comm = Comm(**args)
    439 
    440     @observe('comm')

~/miniconda3/envs/ipydatagrid/lib/python3.7/site-packages/ipykernel/comm/comm.py in __init__(self, target_name, data, metadata, buffers, **kwargs)
     55             if self.primary:
     56                 # I am primary, open my peer.
---> 57                 self.open(data=data, metadata=metadata, buffers=buffers)
     58             else:
     59                 self._closed = False

~/miniconda3/envs/ipydatagrid/lib/python3.7/site-packages/ipykernel/comm/comm.py in open(self, data, metadata, buffers)
     92                               data=data, metadata=metadata, buffers=buffers,
     93                               target_name=self.target_name,
---> 94                               target_module=self.target_module,
     95                               )
     96             self._closed = False

~/miniconda3/envs/ipydatagrid/lib/python3.7/site-packages/ipykernel/comm/comm.py in _publish_msg(self, msg_type, data, metadata, buffers, **keys)
     69             parent=self.kernel.get_parent("shell"),
     70             ident=self.topic,
---> 71             buffers=buffers,
     72         )
     73 

~/miniconda3/envs/ipydatagrid/lib/python3.7/site-packages/jupyter_client/session.py in send(self, stream, msg_or_type, content, parent, ident, buffers, track, header, metadata)
    821         if self.adapt_version:
    822             msg = adapt(msg, self.adapt_version)
--> 823         to_send = self.serialize(msg, ident)
    824         to_send.extend(buffers)
    825         longest = max([len(s) for s in to_send])

~/miniconda3/envs/ipydatagrid/lib/python3.7/site-packages/jupyter_client/session.py in serialize(self, msg, ident)
    695             content = self.none
    696         elif isinstance(content, dict):
--> 697             content = self.pack(content)
    698         elif isinstance(content, bytes):
    699             # content is already packed, as in a relayed message

~/miniconda3/envs/ipydatagrid/lib/python3.7/site-packages/jupyter_client/session.py in json_packer(obj)
     97         default=json_default,
     98         ensure_ascii=False,
---> 99         allow_nan=False,
    100     ).encode("utf8")
    101 

~/miniconda3/envs/ipydatagrid/lib/python3.7/json/__init__.py in dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, default, sort_keys, **kw)
    236         check_circular=check_circular, allow_nan=allow_nan, indent=indent,
    237         separators=separators, default=default, sort_keys=sort_keys,
--> 238         **kw).encode(obj)
    239 
    240 

~/miniconda3/envs/ipydatagrid/lib/python3.7/json/encoder.py in encode(self, o)
    197         # exceptions aren't as detailed.  The list call should be roughly
    198         # equivalent to the PySequence_Fast that ''.join() would do.
--> 199         chunks = self.iterencode(o, _one_shot=True)
    200         if not isinstance(chunks, (list, tuple)):
    201             chunks = list(chunks)

~/miniconda3/envs/ipydatagrid/lib/python3.7/json/encoder.py in iterencode(self, o, _one_shot)
    255                 self.key_separator, self.item_separator, self.sort_keys,
    256                 self.skipkeys, _one_shot)
--> 257         return _iterencode(o, 0)
    258 
    259 def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,

TypeError: keys must be str, int, float, bool or None, not tuple

cc @martinRenou

ibdafna commented 3 years ago

Seems to be caused by this serialisation function in ipywidgets: https://github.com/jupyter-widgets/ipywidgets/blob/3558ce6a22b4a6b1badb69f0507a6ac5b245a17f/ipywidgets/widgets/widget.py#L157

ibdafna commented 3 years ago

Fixed via #274.