Repository: jupyter/nbclient (language: python)

Issue:
Missing dependency on ipython_genutils
* Version: 0.5.12
```
/tmp/tmp.TFAhsvThle
⟩ python3 -m venv env

/tmp/tmp.TFAhsvThle
⟩ source env/bin/activate.fish
(env) 
[env] /tmp/tmp.TFAhsvThle
⟩ pip install nbclient
Collecting nbclient
  Downloading nbclient-0.5.12-py3-none-any.whl (71 kB)
     |████████████████████████████████| 71 kB 2.8 MB/s 
Collecting traitlets>=5.0.0
  Using cached traitlets-5.1.1-py3-none-any.whl (102 kB)
Collecting nbformat>=5.0
  Downloading nbformat-5.2.0-py3-none-any.whl (74 kB)
     |████████████████████████████████| 74 kB 1.5 MB/s 
Collecting nest-asyncio
  Using cached nest_asyncio-1.5.4-py3-none-any.whl (5.1 kB)
Collecting jupyter-client>=6.1.5
  Using cached jupyter_client-7.1.2-py3-none-any.whl (130 kB)
Collecting jupyter-core
  Using cached jupyter_core-4.9.2-py3-none-any.whl (86 kB)
Collecting jsonschema!=2.5.0,>=2.4
  Using cached jsonschema-4.4.0-py3-none-any.whl (72 kB)
Collecting tornado>=4.1
  Using cached tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl (427 kB)
Collecting python-dateutil>=2.1
  Using cached python_dateutil-2.8.2-py2.py3-none-any.whl (247 kB)
Collecting pyzmq>=13
  Using cached pyzmq-22.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (1.1 MB)
Collecting entrypoints
  Using cached entrypoints-0.4-py3-none-any.whl (5.3 kB)
Collecting importlib-resources>=1.4.0; python_version < "3.9"
  Using cached importlib_resources-5.4.0-py3-none-any.whl (28 kB)
Collecting attrs>=17.4.0
  Using cached attrs-21.4.0-py2.py3-none-any.whl (60 kB)
Collecting pyrsistent!=0.17.0,!=0.17.1,!=0.17.2,>=0.14.0
  Using cached pyrsistent-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (119 kB)
Collecting six>=1.5
  Using cached six-1.16.0-py2.py3-none-any.whl (11 kB)
Collecting zipp>=3.1.0; python_version < "3.10"
  Using cached zipp-3.7.0-py3-none-any.whl (5.3 kB)
Installing collected packages: traitlets, jupyter-core, zipp, importlib-resources, attrs, pyrsistent, jsonschema, nbformat, nest-asyncio, tornado, six, python-dateutil, pyzmq, entrypoints, jupyter-client, nbclient
Successfully installed attrs-21.4.0 entrypoints-0.4 importlib-resources-5.4.0 jsonschema-4.4.0 jupyter-client-7.1.2 jupyter-core-4.9.2 nbclient-0.5.12 nbformat-5.2.0 nest-asyncio-1.5.4 pyrsistent-0.18.1 python-dateutil-2.8.2 pyzmq-22.3.0 six-1.16.0 tornado-6.1 traitlets-5.1.1 zipp-3.7.0
(env) 
[env] /tmp/tmp.TFAhsvThle
⟩ python3
Python 3.8.10 (default, Nov 26 2021, 20:14:08) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from nbclient import NotebookClient
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/tmp/tmp.TFAhsvThle/env/lib/python3.8/site-packages/nbclient/__init__.py", line 6, in <module>
    from .client import NotebookClient, execute  # noqa: F401
  File "/tmp/tmp.TFAhsvThle/env/lib/python3.8/site-packages/nbclient/client.py", line 39, in <module>
    from .output_widget import OutputWidget
  File "/tmp/tmp.TFAhsvThle/env/lib/python3.8/site-packages/nbclient/output_widget.py", line 6, in <module>
    from .jsonutil import json_clean
  File "/tmp/tmp.TFAhsvThle/env/lib/python3.8/site-packages/nbclient/jsonutil.py", line 16, in <module>
    from ipython_genutils import py3compat
ModuleNotFoundError: No module named 'ipython_genutils'
```


Discussion / hints:

Fix patch:
diff --git a/nbclient/jsonutil.py b/nbclient/jsonutil.py
index 9a6e0a74..e67ffd5f 100644
--- a/nbclient/jsonutil.py
+++ b/nbclient/jsonutil.py
@@ -13,11 +13,6 @@
 from datetime import datetime
 from typing import Dict
 
-from ipython_genutils import py3compat
-from ipython_genutils.py3compat import iteritems, unicode_type
-
-next_attr_name = '__next__' if py3compat.PY3 else 'next'
-
 # -----------------------------------------------------------------------------
 # Globals and constants
 # -----------------------------------------------------------------------------
@@ -71,44 +66,7 @@ def encode_images(format_dict: Dict) -> Dict[str, str]:
         is base64-encoded.
 
     """
-
-    # no need for handling of ambiguous bytestrings on Python 3,
-    # where bytes objects always represent binary data and thus
-    # base64-encoded.
-    if py3compat.PY3:
-        return format_dict
-
-    encoded = format_dict.copy()
-
-    pngdata = format_dict.get('image/png')
-    if isinstance(pngdata, bytes):
-        # make sure we don't double-encode
-        if not pngdata.startswith(PNG64):
-            pngdata = b2a_base64(pngdata)
-        encoded['image/png'] = pngdata.decode('ascii')
-
-    jpegdata = format_dict.get('image/jpeg')
-    if isinstance(jpegdata, bytes):
-        # make sure we don't double-encode
-        if not jpegdata.startswith(JPEG64):
-            jpegdata = b2a_base64(jpegdata)
-        encoded['image/jpeg'] = jpegdata.decode('ascii')
-
-    gifdata = format_dict.get('image/gif')
-    if isinstance(gifdata, bytes):
-        # make sure we don't double-encode
-        if not gifdata.startswith((GIF_64, GIF89_64)):
-            gifdata = b2a_base64(gifdata)
-        encoded['image/gif'] = gifdata.decode('ascii')
-
-    pdfdata = format_dict.get('application/pdf')
-    if isinstance(pdfdata, bytes):
-        # make sure we don't double-encode
-        if not pdfdata.startswith(PDF64):
-            pdfdata = b2a_base64(pdfdata)
-        encoded['application/pdf'] = pdfdata.decode('ascii')
-
-    return encoded
+    return format_dict
 
 
 def json_clean(obj):
@@ -135,7 +93,7 @@ def json_clean(obj):
 
     """
     # types that are 'atomic' and ok in json as-is.
-    atomic_ok = (unicode_type, type(None))
+    atomic_ok = (str, type(None))
 
     # containers that we need to convert into lists
     container_to_list = (tuple, set, types.GeneratorType)
@@ -160,22 +118,10 @@ def json_clean(obj):
         return obj
 
     if isinstance(obj, bytes):
-        if py3compat.PY3:
-            # unanmbiguous binary data is base64-encoded
-            # (this probably should have happened upstream)
-            return b2a_base64(obj).decode('ascii')
-        else:
-            # Python 2 bytestr is ambiguous,
-            # needs special handling for possible binary bytestrings.
-            # imperfect workaround: if ascii, assume text.
-            # otherwise assume binary, base64-encode (py3 behavior).
-            try:
-                return obj.decode('ascii')
-            except UnicodeDecodeError:
-                return b2a_base64(obj).decode('ascii')
+        return b2a_base64(obj).decode('ascii')
 
     if isinstance(obj, container_to_list) or (
-        hasattr(obj, '__iter__') and hasattr(obj, next_attr_name)
+        hasattr(obj, '__iter__') and hasattr(obj, '__next__')
     ):
         obj = list(obj)
 
@@ -187,7 +133,7 @@ def json_clean(obj):
         # key collisions after stringification.  This can happen with keys like
         # True and 'true' or 1 and '1', which collide in JSON.
         nkeys = len(obj)
-        nkeys_collapsed = len(set(map(unicode_type, obj)))
+        nkeys_collapsed = len(set(map(str, obj)))
         if nkeys != nkeys_collapsed:
             raise ValueError(
                 'dict cannot be safely converted to JSON: '
@@ -195,8 +141,8 @@ def json_clean(obj):
             )
         # If all OK, proceed by making the new dict that will be json-safe
         out = {}
-        for k, v in iteritems(obj):
-            out[unicode_type(k)] = json_clean(v)
+        for k, v in iter(obj.items()):
+            out[str(k)] = json_clean(v)
         return out
     if isinstance(obj, datetime):
         return obj.strftime(ISO8601)
diff --git a/pyproject.toml b/pyproject.toml
index 77b52db1..6bfca656 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,7 +37,6 @@ python_version = 3.9
 
 [[tool.mypy.overrides]]
 module = [
-    "ipython_genutils.*",
     "nbformat.*",
     "nest_asyncio.*",
     "async_generator.*",


Test patch:
diff --git a/nbclient/tests/test_client.py b/nbclient/tests/test_client.py
index 8be9e1b9..50085963 100644
--- a/nbclient/tests/test_client.py
+++ b/nbclient/tests/test_client.py
@@ -14,7 +14,6 @@
 import nbformat
 import pytest
 import xmltodict  # type: ignore
-from ipython_genutils.py3compat import string_types
 from jupyter_client import KernelManager
 from jupyter_client.kernelspec import KernelSpecManager
 from nbconvert.filters import strip_ansi  # type: ignore
@@ -215,7 +214,7 @@ def normalize_output(output):
     if 'image/svg+xml' in output.get('data', {}):
         output['data']['image/svg+xml'] = xmltodict.parse(output['data']['image/svg+xml'])
     for key, value in output.get('data', {}).items():
-        if isinstance(value, string_types):
+        if isinstance(value, str):
             output['data'][key] = normalize_base64(value)
     if 'traceback' in output:
         tb = []