diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 56ce243c7c70..8ab63b920b96 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -761,11 +761,18 @@ def is_url(filename): return URL_REGEX.match(filename) is not None +@functools.lru_cache() +def _get_ssl_context(): + import certifi + import ssl + return ssl.create_default_context(cafile=certifi.where()) + + @contextlib.contextmanager def _open_file_or_url(fname): if not isinstance(fname, Path) and is_url(fname): import urllib.request - with urllib.request.urlopen(fname) as f: + with urllib.request.urlopen(fname, context=_get_ssl_context()) as f: yield (line.decode('utf-8') for line in f) else: fname = os.path.expanduser(fname) diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index d6affc4bfede..d6c2df8a48ca 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -8,7 +8,6 @@ import logging from numbers import Number from pathlib import Path -import urllib.parse import numpy as np import PIL.PngImagePlugin @@ -1443,9 +1442,12 @@ def imread(fname, format=None): - (M, N, 3) for RGB images. - (M, N, 4) for RGBA images. """ + # hide imports to speed initial import on systems with slow linkers + from urllib import parse + if format is None: if isinstance(fname, str): - parsed = urllib.parse.urlparse(fname) + parsed = parse.urlparse(fname) # If the string is a URL (Windows paths appear as if they have a # length-1 scheme), assume png. if len(parsed.scheme) > 1: @@ -1468,10 +1470,18 @@ def imread(fname, format=None): img_open = ( PIL.PngImagePlugin.PngImageFile if ext == 'png' else PIL.Image.open) if isinstance(fname, str): - parsed = urllib.parse.urlparse(fname) + + parsed = parse.urlparse(fname) if len(parsed.scheme) > 1: # Pillow doesn't handle URLs directly. + # hide imports to speed initial import on systems with slow linkers from urllib import request - with urllib.request.urlopen(fname) as response: + with request.urlopen(fname, + context=mpl._get_ssl_context()) as response: + import io + try: + response.seek(0) + except (AttributeError, io.UnsupportedOperation): + response = io.BytesIO(response.read()) return imread(response, format=ext) with img_open(fname) as image: return (_pil_png_to_float_array(image) diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py index 391dd5d49d38..de8a61bdfd64 100644 --- a/lib/matplotlib/testing/conftest.py +++ b/lib/matplotlib/testing/conftest.py @@ -16,6 +16,7 @@ def pytest_configure(config): ("markers", "style: Set alternate Matplotlib style temporarily."), ("markers", "baseline_images: Compare output against references."), ("markers", "pytz: Tests that require pytz to be installed."), + ("markers", "network: Tests that reach out to the network."), ("filterwarnings", "error"), ]: config.addinivalue_line(key, value) diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 7a3c6d844d30..635d30900349 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -1118,3 +1118,9 @@ def test_exact_vmin(): # check than the RBGA values are the same assert np.all(from_image == direct_computation) + + +@pytest.mark.network +@pytest.mark.flaky +def test_https_imread_smoketest(): + v = mimage.imread('https://matplotlib.org/1.5.0/_static/logo2.png') diff --git a/requirements/testing/travis_all.txt b/requirements/testing/travis_all.txt index 3f42a603f6b7..8ee54ccdb905 100644 --- a/requirements/testing/travis_all.txt +++ b/requirements/testing/travis_all.txt @@ -1,5 +1,6 @@ # pip requirements for all the travis builds +certifi coverage pytest!=4.6.0,!=5.4.0 pytest-cov diff --git a/setup.py b/setup.py index 7f08fa09d6eb..e0b4c90e2382 100644 --- a/setup.py +++ b/setup.py @@ -278,6 +278,7 @@ def build_extensions(self): "numpy>=1.15", ], install_requires=[ + "certifi>=2020.06.20", "cycler>=0.10", "kiwisolver>=1.0.1", "numpy>=1.16",