From ad53fe3021ba861f8cd754a82903643b6b8cf01a Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 8 Aug 2024 12:39:47 +0300 Subject: [PATCH 1/2] gh-122255: Synchronize warnings in C and Python implementations of the warnings module In the linecache module and in the Python implementation of the warnings module, a DeprecationWarning is issued when m.__loader__ differs from m.__spec__.loader (like in the C implementation of the warnings module). --- Lib/linecache.py | 64 ++++++++++++++----- Lib/test/test_linecache.py | 32 ++++++++-- Lib/test/test_warnings/__init__.py | 5 +- ...-08-08-12-39-36.gh-issue-122255.J_gU8Y.rst | 4 ++ 4 files changed, 81 insertions(+), 24 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-08-08-12-39-36.gh-issue-122255.J_gU8Y.rst diff --git a/Lib/linecache.py b/Lib/linecache.py index 4b38a0464d8747..11c5ceb536375a 100644 --- a/Lib/linecache.py +++ b/Lib/linecache.py @@ -175,21 +175,55 @@ def lazycache(filename, module_globals): return False if not filename or (filename.startswith('<') and filename.endswith('>')): return False - # Try for a __loader__, if available - if module_globals and '__name__' in module_globals: - spec = module_globals.get('__spec__') - name = getattr(spec, 'name', None) or module_globals['__name__'] - loader = getattr(spec, 'loader', None) - if loader is None: - loader = module_globals.get('__loader__') - get_source = getattr(loader, 'get_source', None) - - if name and get_source: - def get_lines(name=name, *args, **kwargs): - return get_source(name, *args, **kwargs) - cache[filename] = (get_lines,) - return True - return False + + if not isinstance(module_globals, dict): + return False + if not module_globals or '__name__' not in module_globals: + return False + + spec = module_globals.get('__spec__') + name = getattr(spec, 'name', None) or module_globals['__name__'] + if name is None: + return False + + loader = _bless_my_loader(module_globals) + if loader is None: + return False + + get_source = getattr(loader, 'get_source', None) + if get_source is None: + return False + + def get_lines(name=name, *args, **kwargs): + return get_source(name, *args, **kwargs) + cache[filename] = (get_lines,) + return True + +def _bless_my_loader(module_globals): + # Similar to _bless_my_loader() in importlib._bootstrap_external, + # but always emits warnings instead of errors. + loader = module_globals.get('__loader__') + if loader is None and '__spec__' not in module_globals: + return None + + spec = module_globals.get('__spec__') + spec_loader = getattr(spec, 'loader', None) + if spec_loader is None: + import warnings + warnings.warn( + 'Module globals is missing a __spec__.loader', + DeprecationWarning) + return loader + + assert spec_loader is not None + if loader is not None and loader != spec_loader: + import warnings + warnings.warn( + 'Module globals; __loader__ != __spec__.loader', + DeprecationWarning) + return loader + + return spec_loader def _register_code(code, string, name): diff --git a/Lib/test/test_linecache.py b/Lib/test/test_linecache.py index 6f5955791407ea..b01b4768cf7621 100644 --- a/Lib/test/test_linecache.py +++ b/Lib/test/test_linecache.py @@ -256,22 +256,44 @@ def raise_memoryerror(*args, **kwargs): def test_loader(self): filename = 'scheme://path' - for loader in (None, object(), NoSourceLoader()): + linecache.clearcache() + module_globals = {'__name__': 'a.b.c', '__loader__': None} + self.assertEqual(linecache.getlines(filename, module_globals), []) + + for loader in object(), NoSourceLoader(): linecache.clearcache() module_globals = {'__name__': 'a.b.c', '__loader__': loader} - self.assertEqual(linecache.getlines(filename, module_globals), []) + with self.assertWarns(DeprecationWarning) as w: + self.assertEqual(linecache.getlines(filename, module_globals), []) + self.assertEqual(str(w.warning), + 'Module globals is missing a __spec__.loader') linecache.clearcache() module_globals = {'__name__': 'a.b.c', '__loader__': FakeLoader()} - self.assertEqual(linecache.getlines(filename, module_globals), - ['source for a.b.c\n']) + with self.assertWarns(DeprecationWarning) as w: + self.assertEqual(linecache.getlines(filename, module_globals), + ['source for a.b.c\n']) + self.assertEqual(str(w.warning), + 'Module globals is missing a __spec__.loader') - for spec in (None, object(), ModuleSpec('', FakeLoader())): + for spec in None, object(): linecache.clearcache() module_globals = {'__name__': 'a.b.c', '__loader__': FakeLoader(), '__spec__': spec} + with self.assertWarns(DeprecationWarning) as w: + self.assertEqual(linecache.getlines(filename, module_globals), + ['source for a.b.c\n']) + self.assertEqual(str(w.warning), + 'Module globals is missing a __spec__.loader') + + linecache.clearcache() + module_globals = {'__name__': 'a.b.c', '__loader__': FakeLoader(), + '__spec__': ModuleSpec('', FakeLoader())} + with self.assertWarns(DeprecationWarning) as w: self.assertEqual(linecache.getlines(filename, module_globals), ['source for a.b.c\n']) + self.assertEqual(str(w.warning), + 'Module globals; __loader__ != __spec__.loader') linecache.clearcache() spec = ModuleSpec('x.y.z', FakeLoader()) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 8b59630717e790..90b552c0857bc7 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -652,7 +652,7 @@ def check_module_globals(self, module_globals): def check_module_globals_error(self, module_globals, errmsg, errtype=ValueError): if self.module is py_warnings: - self.check_module_globals(module_globals) + self.check_module_globals_deprecated(module_globals, errmsg) return with original_warnings.catch_warnings(module=self.module, record=True) as w: self.module.filterwarnings('always') @@ -663,9 +663,6 @@ def check_module_globals_error(self, module_globals, errmsg, errtype=ValueError) self.assertEqual(len(w), 0) def check_module_globals_deprecated(self, module_globals, msg): - if self.module is py_warnings: - self.check_module_globals(module_globals) - return with original_warnings.catch_warnings(module=self.module, record=True) as w: self.module.filterwarnings('always') self.module.warn_explicit( diff --git a/Misc/NEWS.d/next/Library/2024-08-08-12-39-36.gh-issue-122255.J_gU8Y.rst b/Misc/NEWS.d/next/Library/2024-08-08-12-39-36.gh-issue-122255.J_gU8Y.rst new file mode 100644 index 00000000000000..c71a92246efb56 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-08-08-12-39-36.gh-issue-122255.J_gU8Y.rst @@ -0,0 +1,4 @@ +In the :mod:`linecache` module and in the Python implementation of the +:mod:`warnings` module, a ``DeprecationWarning`` is issued when +``m.__loader__`` differs from ``m.__spec__.loader`` (like in the C +implementation of the :mod:`!warnings` module). From 72042d9fe3b2c1befc829404fe01cf76b95a9f14 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 14 Aug 2024 16:35:44 +0300 Subject: [PATCH 2/2] Fix test_warnings. --- Lib/linecache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/linecache.py b/Lib/linecache.py index 11c5ceb536375a..c2fa42021b45e2 100644 --- a/Lib/linecache.py +++ b/Lib/linecache.py @@ -176,8 +176,8 @@ def lazycache(filename, module_globals): if not filename or (filename.startswith('<') and filename.endswith('>')): return False - if not isinstance(module_globals, dict): - return False + if module_globals is not None and not isinstance(module_globals, dict): + raise TypeError(f'module_globals must be a dict, not {type(module_globals).__name__}') if not module_globals or '__name__' not in module_globals: return False