From f1d2ac85908c997ecc02af75f6a911a05289b492 Mon Sep 17 00:00:00 2001 From: William Schwartz Date: Wed, 2 Dec 2020 00:28:03 -0600 Subject: [PATCH 1/4] Fix bpo-42531: Teach importlib.resources.path to handle packages without __file__ --- Lib/importlib/resources.py | 8 +++++--- Lib/test/test_importlib/test_path.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Lib/importlib/resources.py b/Lib/importlib/resources.py index fc3a1c9cabe636b..8d37d52cb8d37ef 100644 --- a/Lib/importlib/resources.py +++ b/Lib/importlib/resources.py @@ -193,9 +193,11 @@ def path(package: Package, resource: Resource) -> Iterator[Path]: _check_location(package) # Fall-through for both the lack of resource_path() *and* if # resource_path() raises FileNotFoundError. - package_directory = Path(package.__spec__.origin).parent - file_path = package_directory / resource - if file_path.exists(): + file_path = None + if package.__spec__.origin is not None: + package_directory = Path(package.__spec__.origin).parent + file_path = package_directory / resource + if file_path is not None and file_path.exists(): yield file_path else: with open_binary(package, resource) as fp: diff --git a/Lib/test/test_importlib/test_path.py b/Lib/test/test_importlib/test_path.py index 2d3dcda7ed2e799..e19e484797a6c3f 100644 --- a/Lib/test/test_importlib/test_path.py +++ b/Lib/test/test_importlib/test_path.py @@ -1,3 +1,4 @@ +from test.support import swap_attr import unittest from importlib import resources @@ -26,6 +27,17 @@ def test_reading(self): class PathDiskTests(PathTests, unittest.TestCase): data = data01 + def test_package_spec_origin_is_None(self): + import pydoc_data + spec = pydoc_data.__spec__ + # Emulate importing from non-file source by setting spec.origin = None. + # Barge past path's sanity checks by ensuring spec.loader.is_resource + # returns False. + with swap_attr(spec, "origin", None), \ + swap_attr(spec.loader, "is_resource", lambda *args: False), \ + resources.path(pydoc_data, '_pydoc.css') as p: + pass + class PathZipTests(PathTests, util.ZipSetup, unittest.TestCase): def test_remove_in_context_manager(self): From a01d37b567e5d6140e7478fd204ae3058bad0011 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Wed, 2 Dec 2020 16:28:05 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NEWS.d/next/Library/2020-12-02-16-28-04.bpo-42531.2sLlFW.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2020-12-02-16-28-04.bpo-42531.2sLlFW.rst diff --git a/Misc/NEWS.d/next/Library/2020-12-02-16-28-04.bpo-42531.2sLlFW.rst b/Misc/NEWS.d/next/Library/2020-12-02-16-28-04.bpo-42531.2sLlFW.rst new file mode 100644 index 000000000000000..7927078acda430d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-12-02-16-28-04.bpo-42531.2sLlFW.rst @@ -0,0 +1 @@ +:func:`importlib.resources.path` now works for :term:`package`\ s missing the optional :attr:`__file__` attribute (more specifically, packages whose :attr:`__spec__`\ ``.``\ :attr:`~importlib.machinery.ModuleSpec.origin` :keyword:`is` :data:`None`). \ No newline at end of file From 7176b485bf641efce62f182e0369b004b673f199 Mon Sep 17 00:00:00 2001 From: William Schwartz Date: Mon, 11 Jan 2021 16:27:53 -0600 Subject: [PATCH 3/4] Fix test per review comments https://github.com/python/cpython/pull/23611#pullrequestreview-564816417 This creates a package in memory and sets its __spec__.origin to None manually. Otherwise the test proceeds like the others PathTests subclasses. --- Lib/test/test_importlib/test_path.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_importlib/test_path.py b/Lib/test/test_importlib/test_path.py index e19e484797a6c3f..0f62ae1dac89764 100644 --- a/Lib/test/test_importlib/test_path.py +++ b/Lib/test/test_importlib/test_path.py @@ -1,4 +1,4 @@ -from test.support import swap_attr +import io import unittest from importlib import resources @@ -13,6 +13,8 @@ def execute(self, package, path): class PathTests: + UTF_8_FILE_CONTENTS = b'Hello, UTF-8 world!\n' + def test_reading(self): # Path should be readable. # Test also implicitly verifies the returned object is a pathlib.Path @@ -21,22 +23,21 @@ def test_reading(self): # pathlib.Path.read_text() was introduced in Python 3.5. with path.open('r', encoding='utf-8') as file: text = file.read() - self.assertEqual('Hello, UTF-8 world!\n', text) + self.assertEqual(self.UTF_8_FILE_CONTENTS.decode(), text) class PathDiskTests(PathTests, unittest.TestCase): data = data01 - def test_package_spec_origin_is_None(self): - import pydoc_data - spec = pydoc_data.__spec__ - # Emulate importing from non-file source by setting spec.origin = None. - # Barge past path's sanity checks by ensuring spec.loader.is_resource - # returns False. - with swap_attr(spec, "origin", None), \ - swap_attr(spec.loader, "is_resource", lambda *args: False), \ - resources.path(pydoc_data, '_pydoc.css') as p: - pass + +class PathMemoryTests(PathTests, unittest.TestCase): + def setUp(self): + file = io.BytesIO(self.UTF_8_FILE_CONTENTS) + self.addCleanup(file.close) + self.data = util.create_package( + file=file, path=FileNotFoundError("package exists only in memory")) + self.data.__spec__.origin = None + self.data.__spec__.has_location = False class PathZipTests(PathTests, util.ZipSetup, unittest.TestCase): From 96005204f0544372a7889d8e4b22cba2992593f5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 Jan 2021 11:23:28 -0500 Subject: [PATCH 4/4] Apply tweaks to decouple tests in test_importlib.test_path and apply black formatting. --- Lib/test/test_importlib/test_path.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_importlib/test_path.py b/Lib/test/test_importlib/test_path.py index 0f62ae1dac89764..5c5a8e3d76e79e0 100644 --- a/Lib/test/test_importlib/test_path.py +++ b/Lib/test/test_importlib/test_path.py @@ -13,8 +13,6 @@ def execute(self, package, path): class PathTests: - UTF_8_FILE_CONTENTS = b'Hello, UTF-8 world!\n' - def test_reading(self): # Path should be readable. # Test also implicitly verifies the returned object is a pathlib.Path @@ -23,7 +21,7 @@ def test_reading(self): # pathlib.Path.read_text() was introduced in Python 3.5. with path.open('r', encoding='utf-8') as file: text = file.read() - self.assertEqual(self.UTF_8_FILE_CONTENTS.decode(), text) + self.assertEqual('Hello, UTF-8 world!\n', text) class PathDiskTests(PathTests, unittest.TestCase): @@ -32,10 +30,11 @@ class PathDiskTests(PathTests, unittest.TestCase): class PathMemoryTests(PathTests, unittest.TestCase): def setUp(self): - file = io.BytesIO(self.UTF_8_FILE_CONTENTS) + file = io.BytesIO(b'Hello, UTF-8 world!\n') self.addCleanup(file.close) self.data = util.create_package( - file=file, path=FileNotFoundError("package exists only in memory")) + file=file, path=FileNotFoundError("package exists only in memory") + ) self.data.__spec__.origin = None self.data.__spec__.has_location = False