From 5d5d919c09b140a3cda54c99fc132b26e254ba68 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 5 Aug 2020 15:14:38 -0400 Subject: [PATCH 1/3] FIX: fix reading from http/https urls via imread closes #18129 Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/image.py | 17 +++++++++++++---- lib/matplotlib/testing/conftest.py | 1 + lib/matplotlib/tests/test_image.py | 6 ++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index d6affc4bfede..edb347067d60 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,17 @@ 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) 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') From 978e0d466845bbd5101de751bac23e1d703723dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Fri, 7 Aug 2020 08:16:00 +0300 Subject: [PATCH 2/3] Fix ssl fetching on older systems certifi was already an indirect dependency but make it explicit --- lib/matplotlib/__init__.py | 9 ++++++++- lib/matplotlib/image.py | 3 ++- setup.py | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) 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 edb347067d60..d6c2df8a48ca 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -1475,7 +1475,8 @@ def imread(fname, format=None): 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 request.urlopen(fname) as response: + with request.urlopen(fname, + context=mpl._get_ssl_context()) as response: import io try: response.seek(0) 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", From b71024e344b901cba02f24f169f726bf90e9a763 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 10 Aug 2020 16:40:08 -0400 Subject: [PATCH 3/3] TST: make sure certifi is actually installed for tests --- requirements/testing/travis_all.txt | 1 + 1 file changed, 1 insertion(+) 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