Repository: SciTools/cartopy

Issue:
Python 3.8 Fails to render projection
Doing this in a Jupyter cell on Python 3.8:
```python
import cartopy.crs as ccrs
ccrs.PlateCarree()
```

tries to render the projection as an image. However, it fails:
```pytb
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
~/miniconda3/envs/py38/lib/python3.8/site-packages/IPython/core/formatters.py in __call__(self, obj)
    343             method = get_real_method(obj, self.print_method)
    344             if method is not None:
--> 345                 return method()
    346             return None
    347         else:

~/miniconda3/envs/py38/lib/python3.8/site-packages/cartopy/crs.py in _repr_html_(self)
    180         buf.seek(0)
    181         svg = buf.read()
--> 182         return '{}<pre>{}</pre>'.format(svg, cgi.escape(repr(self)))
    183 
    184     def _as_mpl_axes(self):

AttributeError: module 'cgi' has no attribute 'escape'
```
I'm guessing we need to find a better place from which to pull the `escape` function.


Discussion:
FWIW, my py2+py3 compat helper is:
```python
from six import PY2
def html_escape(val):
    """Wrapper around cgi.escape depreciation."""
    if not PY2:
        from html import escape
    else:
        from cgi import escape
    return escape(val)
```
*sigh* Is there seriously not just a *single* location we can use?

Resolving patch:
diff --git a/lib/cartopy/crs.py b/lib/cartopy/crs.py
--- a/lib/cartopy/crs.py
+++ b/lib/cartopy/crs.py
@@ -154,7 +154,10 @@ def _determine_longitude_bounds(self, central_longitude):
         return minlon, maxlon
 
     def _repr_html_(self):
-        import cgi
+        if not six.PY2:
+            from html import escape
+        else:
+            from cgi import escape
         try:
             # As matplotlib is not a core cartopy dependency, don't error
             # if it's not available.
@@ -176,7 +179,7 @@ def _repr_html_(self):
         # "Rewind" the buffer to the start and return it as an svg string.
         buf.seek(0)
         svg = buf.read()
-        return '{}<pre>{}</pre>'.format(svg, cgi.escape(repr(self)))
+        return '{}<pre>{}</pre>'.format(svg, escape(repr(self)))
 
     def _as_mpl_axes(self):
         import cartopy.mpl.geoaxes as geoaxes
diff --git a/lib/cartopy/mpl/geoaxes.py b/lib/cartopy/mpl/geoaxes.py
--- a/lib/cartopy/mpl/geoaxes.py
+++ b/lib/cartopy/mpl/geoaxes.py
@@ -24,10 +24,16 @@
 
 from __future__ import (absolute_import, division, print_function)
 
+import six
+
 import collections
 import contextlib
 import warnings
 import weakref
+if not six.PY2:
+    import collections.abc as collections_abc
+else:
+    import collections as collections_abc
 
 import matplotlib as mpl
 import matplotlib.artist
@@ -1171,7 +1177,7 @@ def _regrid_shape_aspect(self, regrid_shape, target_extent):
         plotting methods.
 
         """
-        if not isinstance(regrid_shape, collections.Sequence):
+        if not isinstance(regrid_shape, collections_abc.Sequence):
             target_size = int(regrid_shape)
             x_range, y_range = np.diff(target_extent)[::2]
             desired_aspect = x_range / y_range


Test patch:
diff --git a/lib/cartopy/tests/mpl/test_crs.py b/lib/cartopy/tests/mpl/test_crs.py
--- a/lib/cartopy/tests/mpl/test_crs.py
+++ b/lib/cartopy/tests/mpl/test_crs.py
@@ -18,6 +18,7 @@
 from __future__ import (absolute_import, division, print_function)
 
 import matplotlib.pyplot as plt
+from matplotlib.testing.decorators import cleanup
 import pytest
 
 import cartopy.crs as ccrs
@@ -44,3 +45,13 @@ def test_mercator_squashed():
     ax = plt.axes(projection=crs)
     ax.coastlines()
     ax.gridlines()
+
+
+@cleanup
+def test_repr_html():
+    pc = ccrs.PlateCarree()
+    html = pc._repr_html_()
+
+    assert html is not None
+    assert '<svg ' in html
+    assert '<pre>&lt;cartopy.crs.PlateCarree object at ' in html