Skip to content

Using Cartopy with JupyterLite and handling data downloads #2514

@rpwagner

Description

@rpwagner

Hi,

I'm trying to use Cartopy within a JupyterLite notebook and I'm running into an issue with downloads still happening in the background while the plots are still being generated. I haven't found a place or configuration to block until the downloads complete, such as using an await statement.

I'm using the simple maps example of how to use Cartopy with Matplotib

import cartopy.crs as ccrs
import matplotlib.pyplot as plt

ax = plt.axes(projection=ccrs.PlateCarree())
ax.coastlines()

# Save the plot by calling plt.savefig() BEFORE plt.show()
plt.savefig('coastlines.pdf')
plt.savefig('coastlines.png')

plt.show()

When I run this, I first receive a DownloadWarning

/lib/python3.12/site-packages/cartopy/io/__init__.py:241: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/110m_physical/ne_110m_coastline.zip
  warnings.warn(f'Downloading: {url}', DownloadWarning)

This then produces the BlockingIOError error (full error at the bottom):

Error in callback <function _draw_all_if_interactive at 0x677fab8> (for post_execute), with arguments args (),kwargs {}:
...
URLError: <urlopen error [Errno 26] Operation in progress>

Is there a workaround or way to block on the IO, or to prefetch the data? I recognize this could be difficult in the JupyterLite context. If needed, I could prestage some of the data when building the JupyterLite site, but this limits the types of plots users can make when doing exploration.

Thanks

For reference, here are the versions of the packages in this environment:

Pyodide version: 0.27.3
Pyodide kernel version: 0.5.2
Cartopy version: 0.24.1
Matplotlib version: 3.8.4

Full error:

---------------------------------------------------------------------------
BlockingIOError                           Traceback (most recent call last)
File /lib/python312.zip/urllib/request.py:1344, in AbstractHTTPHandler.do_open(self, http_class, req, **http_conn_args)
   1343 try:
-> 1344     h.request(req.get_method(), req.selector, req.data, headers,
   1345               encode_chunked=req.has_header('Transfer-encoding'))
   1346 except OSError as err: # timeout error

File /lib/python312.zip/http/client.py:1336, in HTTPConnection.request(self, method, url, body, headers, encode_chunked)
   1335 """Send a complete request to the server."""
-> 1336 self._send_request(method, url, body, headers, encode_chunked)

File /lib/python312.zip/http/client.py:1382, in HTTPConnection._send_request(self, method, url, body, headers, encode_chunked)
   1381     body = _encode(body, 'body')
-> 1382 self.endheaders(body, encode_chunked=encode_chunked)

File /lib/python312.zip/http/client.py:1331, in HTTPConnection.endheaders(self, message_body, encode_chunked)
   1330     raise CannotSendHeader()
-> 1331 self._send_output(message_body, encode_chunked=encode_chunked)

File /lib/python312.zip/http/client.py:1091, in HTTPConnection._send_output(self, message_body, encode_chunked)
   1090 del self._buffer[:]
-> 1091 self.send(msg)
   1093 if message_body is not None:
   1094 
   1095     # create a consistent interface to message_body

File /lib/python312.zip/http/client.py:1035, in HTTPConnection.send(self, data)
   1034 if self.auto_open:
-> 1035     self.connect()
   1036 else:

File /lib/python312.zip/http/client.py:1470, in HTTPSConnection.connect(self)
   1468 "Connect to a host on a given (SSL) port."
-> 1470 super().connect()
   1472 if self._tunnel_host:

File /lib/python312.zip/http/client.py:1001, in HTTPConnection.connect(self)
   1000 sys.audit("http.client.connect", self, self.host, self.port)
-> 1001 self.sock = self._create_connection(
   1002     (self.host,self.port), self.timeout, self.source_address)
   1003 # Might fail in OSs that don't implement TCP_NODELAY

File /lib/python312.zip/socket.py:865, in create_connection(address, timeout, source_address, all_errors)
    864 if not all_errors:
--> 865     raise exceptions[0]
    866 raise ExceptionGroup("create_connection failed", exceptions)

File /lib/python312.zip/socket.py:850, in create_connection(address, timeout, source_address, all_errors)
    849     sock.bind(source_address)
--> 850 sock.connect(sa)
    851 # Break explicitly a reference cycle

BlockingIOError: [Errno 26] Operation in progress

During handling of the above exception, another exception occurred:

URLError                                  Traceback (most recent call last)
File /lib/python3.12/site-packages/IPython/core/formatters.py:402, in BaseFormatter.__call__(self, obj)
    400     pass
    401 else:
--> 402     return printer(obj)
    403 # Finally look for special method names
    404 method = get_real_method(obj, self.print_method)

File /lib/python3.12/site-packages/IPython/core/pylabtools.py:170, in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
    167     from matplotlib.backend_bases import FigureCanvasBase
    168     FigureCanvasBase(fig)
--> 170 fig.canvas.print_figure(bytes_io, **kw)
    171 data = bytes_io.getvalue()
    172 if fmt == 'svg':

File /lib/python3.12/site-packages/matplotlib/backend_bases.py:2164, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2161     # we do this instead of `self.figure.draw_without_rendering`
   2162     # so that we can inject the orientation
   2163     with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2164         self.figure.draw(renderer)
   2165 if bbox_inches:
   2166     if bbox_inches == "tight":

File /lib/python3.12/site-packages/matplotlib/artist.py:95, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
     93 @wraps(draw)
     94 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 95     result = draw(artist, renderer, *args, **kwargs)
     96     if renderer._rasterizing:
     97         renderer.stop_rasterizing()

File /lib/python3.12/site-packages/matplotlib/artist.py:72, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     69     if artist.get_agg_filter() is not None:
     70         renderer.start_filter()
---> 72     return draw(artist, renderer)
     73 finally:
     74     if artist.get_agg_filter() is not None:

File /lib/python3.12/site-packages/matplotlib/figure.py:3154, in Figure.draw(self, renderer)
   3151         # ValueError can occur when resizing a window.
   3153 self.patch.draw(renderer)
-> 3154 mimage._draw_list_compositing_images(
   3155     renderer, self, artists, self.suppressComposite)
   3157 for sfig in self.subfigs:
   3158     sfig.draw(renderer)

File /lib/python3.12/site-packages/matplotlib/image.py:132, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
    130 if not_composite or not has_images:
    131     for a in artists:
--> 132         a.draw(renderer)
    133 else:
    134     # Composite any adjacent images together
    135     image_group = []

File /lib/python3.12/site-packages/matplotlib/artist.py:72, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     69     if artist.get_agg_filter() is not None:
     70         renderer.start_filter()
---> 72     return draw(artist, renderer)
     73 finally:
     74     if artist.get_agg_filter() is not None:

File /lib/python3.12/site-packages/cartopy/mpl/geoaxes.py:524, in GeoAxes.draw(self, renderer, **kwargs)
    519         self.imshow(img, extent=extent, origin=origin,
    520                     transform=factory.crs, *factory_args[1:],
    521                     **factory_kwargs)
    522 self._done_img_factory = True
--> 524 return super().draw(renderer=renderer, **kwargs)

File /lib/python3.12/site-packages/matplotlib/artist.py:72, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     69     if artist.get_agg_filter() is not None:
     70         renderer.start_filter()
---> 72     return draw(artist, renderer)
     73 finally:
     74     if artist.get_agg_filter() is not None:

File /lib/python3.12/site-packages/matplotlib/axes/_base.py:3070, in _AxesBase.draw(self, renderer)
   3067 if artists_rasterized:
   3068     _draw_rasterized(self.figure, artists_rasterized, renderer)
-> 3070 mimage._draw_list_compositing_images(
   3071     renderer, self, artists, self.figure.suppressComposite)
   3073 renderer.close_group('axes')
   3074 self.stale = False

File /lib/python3.12/site-packages/matplotlib/image.py:132, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
    130 if not_composite or not has_images:
    131     for a in artists:
--> 132         a.draw(renderer)
    133 else:
    134     # Composite any adjacent images together
    135     image_group = []

File /lib/python3.12/site-packages/matplotlib/artist.py:72, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
     69     if artist.get_agg_filter() is not None:
     70         renderer.start_filter()
---> 72     return draw(artist, renderer)
     73 finally:
     74     if artist.get_agg_filter() is not None:

File /lib/python3.12/site-packages/cartopy/mpl/feature_artist.py:185, in FeatureArtist.draw(self, renderer)
    180     geoms = self._feature.geometries()
    181 else:
    182     # For efficiency on local maps with high resolution features (e.g
    183     # from Natural Earth), only create paths for geometries that are
    184     # in view.
--> 185     geoms = self._feature.intersecting_geometries(extent)
    187 stylised_paths = {}
    188 # Make an empty placeholder style dictionary for when styler is not
    189 # used.  Freeze it so that we can use it as a dict key.  We will need
    190 # to unfreeze all style dicts with dict(frozen) before passing to mpl.

File /lib/python3.12/site-packages/cartopy/feature/__init__.py:309, in NaturalEarthFeature.intersecting_geometries(self, extent)
    302 """
    303 Returns an iterator of shapely geometries that intersect with
    304 the given extent.
    305 The extent is assumed to be in the CRS of the feature.
    306 If extent is None, the method returns all geometries for this dataset.
    307 """
    308 self.scaler.scale_from_extent(extent)
--> 309 return super().intersecting_geometries(extent)

File /lib/python3.12/site-packages/cartopy/feature/__init__.py:112, in Feature.intersecting_geometries(self, extent)
    109 if extent is not None and not np.isnan(extent[0]):
    110     extent_geom = sgeom.box(extent[0], extent[2],
    111                             extent[1], extent[3])
--> 112     return (geom for geom in self.geometries() if
    113             geom is not None and extent_geom.intersects(geom))
    114 else:
    115     return self.geometries()

File /lib/python3.12/site-packages/cartopy/feature/__init__.py:291, in NaturalEarthFeature.geometries(self)
    289 key = (self.name, self.category, self.scale)
    290 if key not in _NATURAL_EARTH_GEOM_CACHE:
--> 291     path = shapereader.natural_earth(resolution=self.scale,
    292                                      category=self.category,
    293                                      name=self.name)
    294     geometries = tuple(shapereader.Reader(path).geometries())
    295     _NATURAL_EARTH_GEOM_CACHE[key] = geometries

File /lib/python3.12/site-packages/cartopy/io/shapereader.py:306, in natural_earth(resolution, category, name)
    302 ne_downloader = Downloader.from_config(('shapefiles', 'natural_earth',
    303                                         resolution, category, name))
    304 format_dict = {'config': config, 'category': category,
    305                'name': name, 'resolution': resolution}
--> 306 return ne_downloader.path(format_dict)

File /lib/python3.12/site-packages/cartopy/io/__init__.py:203, in Downloader.path(self, format_dict)
    200     result_path = target_path
    201 else:
    202     # we need to download the file
--> 203     result_path = self.acquire_resource(target_path, format_dict)
    205 return result_path

File /lib/python3.12/site-packages/cartopy/io/shapereader.py:359, in NEShpDownloader.acquire_resource(self, target_path, format_dict)
    355 target_dir.mkdir(parents=True, exist_ok=True)
    357 url = self.url(format_dict)
--> 359 shapefile_online = self._urlopen(url)
    361 zfh = ZipFile(io.BytesIO(shapefile_online.read()), 'r')
    363 for member_path in self.zip_file_contents(format_dict):

File /lib/python3.12/site-packages/cartopy/io/__init__.py:242, in Downloader._urlopen(self, url)
    235 """
    236 Returns a file handle to the given HTTP resource URL.
    237 
    238 Caller should close the file handle when finished with it.
    239 
    240 """
    241 warnings.warn(f'Downloading: {url}', DownloadWarning)
--> 242 return urlopen(url)

File /lib/python312.zip/urllib/request.py:215, in urlopen(url, data, timeout, cafile, capath, cadefault, context)
    213 else:
    214     opener = _opener
--> 215 return opener.open(url, data, timeout)

File /lib/python312.zip/urllib/request.py:515, in OpenerDirector.open(self, fullurl, data, timeout)
    512     req = meth(req)
    514 sys.audit('urllib.Request', req.full_url, req.data, req.headers, req.get_method())
--> 515 response = self._open(req, data)
    517 # post-process response
    518 meth_name = protocol+"_response"

File /lib/python312.zip/urllib/request.py:532, in OpenerDirector._open(self, req, data)
    529     return result
    531 protocol = req.type
--> 532 result = self._call_chain(self.handle_open, protocol, protocol +
    533                           '_open', req)
    534 if result:
    535     return result

File /lib/python312.zip/urllib/request.py:492, in OpenerDirector._call_chain(self, chain, kind, meth_name, *args)
    490 for handler in handlers:
    491     func = getattr(handler, meth_name)
--> 492     result = func(*args)
    493     if result is not None:
    494         return result

File /lib/python312.zip/urllib/request.py:1392, in HTTPSHandler.https_open(self, req)
   1391 def https_open(self, req):
-> 1392     return self.do_open(http.client.HTTPSConnection, req,
   1393                         context=self._context)

File /lib/python312.zip/urllib/request.py:1347, in AbstractHTTPHandler.do_open(self, http_class, req, **http_conn_args)
   1344         h.request(req.get_method(), req.selector, req.data, headers,
   1345                   encode_chunked=req.has_header('Transfer-encoding'))
   1346     except OSError as err: # timeout error
-> 1347         raise URLError(err)
   1348     r = h.getresponse()
   1349 except:

URLError: <urlopen error [Errno 26] Operation in progress>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions