From 198adec064edf0c10aac45ab85c68535ba038a59 Mon Sep 17 00:00:00 2001 From: Gatsik <74517072+Gatsik@users.noreply.github.com> Date: Sat, 17 Aug 2024 00:59:03 +0300 Subject: [PATCH 1/7] gh-121735: Fix inferring caller when resolving importlib.resources.files() without anchor --- importlib_resources/_common.py | 7 +++++- importlib_resources/tests/test_files.py | 31 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 8df6b39..970585e 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -91,9 +91,14 @@ def _infer_caller(): """ Walk the stack and find the frame of the first caller not in this module. """ + this_frame = inspect.currentframe() + if this_frame is None: + this_file = __file__ + else: + this_file = inspect.getframeinfo(this_frame).filename def is_this_file(frame_info): - return frame_info.filename == __file__ + return frame_info.filename == this_file def is_wrapper(frame_info): return frame_info.function == 'wrapper' diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 564b2cc..01b279a 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -1,3 +1,7 @@ +import os +import pathlib +import py_compile +import shutil import textwrap import unittest import warnings @@ -122,6 +126,33 @@ def test_implicit_files_submodule(self): """ assert importlib.import_module('somepkg.submod').val == 'resources are the best' + def _compile_importlib(self, target_dir): + importlib_dir = pathlib.Path(importlib.__file__).parent + shutil.copytree(importlib_dir, target_dir, ignore=lambda *_: ['__pycache__']) + + for dirpath, _, filenames in os.walk(target_dir): + for filename in filenames: + source_path = pathlib.Path(dirpath) / filename + cfile = source_path.with_suffix('.pyc') + py_compile.compile(source_path, cfile) + pathlib.Path.unlink(source_path) + + def test_implicit_files_with_compiled_importlib(self): + self._compile_importlib(pathlib.Path(self.site_dir) / 'cimportlib') + spec = { + 'somepkg': { + '__init__.py': textwrap.dedent( + """ + import cimportlib.resources as res + val = res.files().joinpath('res.txt').read_text(encoding='utf-8') + """ + ), + 'res.txt': 'resources are the best', + }, + } + _path.build(spec, self.site_dir) + assert importlib.import_module('somepkg').val == 'resources are the best' + class ImplicitContextFilesDiskTests( DirectSpec, util.DiskSetup, ImplicitContextFiles, unittest.TestCase From cba8dce7839977c66806ef05e122cf38ed14a113 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 16 Aug 2024 22:27:46 -0400 Subject: [PATCH 2/7] Adapt changes for new fixtures. --- importlib_resources/tests/test_files.py | 35 ++++++++++++------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 01b279a..23b4ad8 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -11,6 +11,7 @@ import importlib_resources as resources from ..abc import Traversable from . import util +from .compat.py39 import os_helper, import_helper @contextlib.contextmanager @@ -112,6 +113,10 @@ class ImplicitContextFiles: 'submod.py': set_val, 'res.txt': 'resources are the best', }, + 'frozenpkg': { + '__init__.py': set_val.replace('importlib_resources', 'c_resources'), + 'res.txt': 'resources are the best', + }, } def test_implicit_files_package(self): @@ -126,32 +131,26 @@ def test_implicit_files_submodule(self): """ assert importlib.import_module('somepkg.submod').val == 'resources are the best' - def _compile_importlib(self, target_dir): - importlib_dir = pathlib.Path(importlib.__file__).parent - shutil.copytree(importlib_dir, target_dir, ignore=lambda *_: ['__pycache__']) + def _compile_importlib(self): + """ + Make a compiled-only copy of the importlib resources package. + """ + bin_site = self.fixtures.enter_context(os_helper.temp_dir()) + c_resources = pathlib.Path(bin_site, 'c_resources') + sources = pathlib.Path(resources.__file__).parent + shutil.copytree(sources, c_resources, ignore=lambda *_: ['__pycache__']) - for dirpath, _, filenames in os.walk(target_dir): + for dirpath, _, filenames in os.walk(c_resources): for filename in filenames: source_path = pathlib.Path(dirpath) / filename cfile = source_path.with_suffix('.pyc') py_compile.compile(source_path, cfile) pathlib.Path.unlink(source_path) + self.fixtures.enter_context(import_helper.DirsOnSysPath(bin_site)) def test_implicit_files_with_compiled_importlib(self): - self._compile_importlib(pathlib.Path(self.site_dir) / 'cimportlib') - spec = { - 'somepkg': { - '__init__.py': textwrap.dedent( - """ - import cimportlib.resources as res - val = res.files().joinpath('res.txt').read_text(encoding='utf-8') - """ - ), - 'res.txt': 'resources are the best', - }, - } - _path.build(spec, self.site_dir) - assert importlib.import_module('somepkg').val == 'resources are the best' + self._compile_importlib() + assert importlib.import_module('frozenpkg').val == 'resources are the best' class ImplicitContextFilesDiskTests( From 4ea81bf920f6cc6377ccc7fbc1f4f343927f4f20 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 16 Aug 2024 23:04:07 -0400 Subject: [PATCH 3/7] Extract a function for computing 'this filename' once. --- importlib_resources/_common.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 970585e..364e4c0 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -87,18 +87,19 @@ def _(cand: None) -> types.ModuleType: return resolve(_infer_caller().f_globals['__name__']) +@functools.lru_cache +def _this_filename(): + frame = inspect.currentframe() + return __file__ if frame is None else inspect.getframeinfo(frame).filename + + def _infer_caller(): """ Walk the stack and find the frame of the first caller not in this module. """ - this_frame = inspect.currentframe() - if this_frame is None: - this_file = __file__ - else: - this_file = inspect.getframeinfo(this_frame).filename def is_this_file(frame_info): - return frame_info.filename == this_file + return frame_info.filename == _this_filename() def is_wrapper(frame_info): return frame_info.function == 'wrapper' From ebc5b97ffe1d20eb6ebc536dd1f6b385f83652a5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 17 Aug 2024 08:04:42 -0400 Subject: [PATCH 4/7] Extract the filename from the topmost frame of the stack. --- importlib_resources/_common.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/importlib_resources/_common.py b/importlib_resources/_common.py index 364e4c0..e95371c 100644 --- a/importlib_resources/_common.py +++ b/importlib_resources/_common.py @@ -87,24 +87,19 @@ def _(cand: None) -> types.ModuleType: return resolve(_infer_caller().f_globals['__name__']) -@functools.lru_cache -def _this_filename(): - frame = inspect.currentframe() - return __file__ if frame is None else inspect.getframeinfo(frame).filename - - def _infer_caller(): """ Walk the stack and find the frame of the first caller not in this module. """ def is_this_file(frame_info): - return frame_info.filename == _this_filename() + return frame_info.filename == stack[0].filename def is_wrapper(frame_info): return frame_info.function == 'wrapper' - not_this_file = itertools.filterfalse(is_this_file, inspect.stack()) + stack = inspect.stack() + not_this_file = itertools.filterfalse(is_this_file, stack) # also exclude 'wrapper' due to singledispatch in the call stack callers = itertools.filterfalse(is_wrapper, not_this_file) return next(callers).frame From d618902dbe0e9f94e634a95bc9bbaf941236fc0c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 17 Aug 2024 08:01:09 -0400 Subject: [PATCH 5/7] Add news fragment. --- newsfragments/+0f77c990.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/+0f77c990.bugfix.rst diff --git a/newsfragments/+0f77c990.bugfix.rst b/newsfragments/+0f77c990.bugfix.rst new file mode 100644 index 0000000..99c0dfc --- /dev/null +++ b/newsfragments/+0f77c990.bugfix.rst @@ -0,0 +1 @@ +When inferring the caller in ``files()`` correctly detect one's own module even when the resources package source is not present. (python/cpython#123085) \ No newline at end of file From 90c0e420ef15256f116342c97ea984a2fa604cc3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 17 Aug 2024 08:08:13 -0400 Subject: [PATCH 6/7] Rely on `resources.__name__` for easier portability. --- importlib_resources/tests/test_files.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 23b4ad8..874874f 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -102,8 +102,8 @@ class ModuleFilesZipTests(DirectSpec, util.ZipSetup, ModulesFiles, unittest.Test class ImplicitContextFiles: set_val = textwrap.dedent( - """ - import importlib_resources as res + f""" + import {resources.__name__} as res val = res.files().joinpath('res.txt').read_text(encoding='utf-8') """ ) @@ -114,7 +114,7 @@ class ImplicitContextFiles: 'res.txt': 'resources are the best', }, 'frozenpkg': { - '__init__.py': set_val.replace('importlib_resources', 'c_resources'), + '__init__.py': set_val.replace(resources.__name__, 'c_resources'), 'res.txt': 'resources are the best', }, } From 79fa62f4b5cbf8f358560651a714b282aee2226c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 17 Aug 2024 08:09:58 -0400 Subject: [PATCH 7/7] Add docstring and reference to the issue. --- importlib_resources/tests/test_files.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/importlib_resources/tests/test_files.py b/importlib_resources/tests/test_files.py index 874874f..9cb55d0 100644 --- a/importlib_resources/tests/test_files.py +++ b/importlib_resources/tests/test_files.py @@ -149,6 +149,11 @@ def _compile_importlib(self): self.fixtures.enter_context(import_helper.DirsOnSysPath(bin_site)) def test_implicit_files_with_compiled_importlib(self): + """ + Caller detection works for compiled-only resources module. + + python/cpython#123085 + """ self._compile_importlib() assert importlib.import_module('frozenpkg').val == 'resources are the best'