Skip to content

Commit e1c4d56

Browse files
authored
gh-65961: Do not rely solely on __cached__ (GH-97990)
Make sure `__spec__.cached` (at minimum) can be used.
1 parent f8edc6f commit e1c4d56

File tree

11 files changed

+130
-35
lines changed

11 files changed

+130
-35
lines changed

Doc/c-api/import.rst

+9
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,11 @@ Importing Modules
150150
See also :c:func:`PyImport_ExecCodeModuleEx` and
151151
:c:func:`PyImport_ExecCodeModuleWithPathnames`.
152152
153+
.. versionchanged:: 3.12
154+
The setting of :attr:`__cached__` and :attr:`__loader__` is
155+
deprecated. See :class:`~importlib.machinery.ModuleSpec` for
156+
alternatives.
157+
153158
154159
.. c:function:: PyObject* PyImport_ExecCodeModuleEx(const char *name, PyObject *co, const char *pathname)
155160
@@ -167,6 +172,10 @@ Importing Modules
167172
168173
.. versionadded:: 3.3
169174
175+
.. versionchanged:: 3.12
176+
Setting :attr:`__cached__` is deprecated. See
177+
:class:`~importlib.machinery.ModuleSpec` for alternatives.
178+
170179
171180
.. c:function:: PyObject* PyImport_ExecCodeModuleWithPathnames(const char *name, PyObject *co, const char *pathname, const char *cpathname)
172181

Doc/library/runpy.rst

+9
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ The :mod:`runpy` module provides two functions:
9393
run this way, as well as ensuring the real module name is always
9494
accessible as ``__spec__.name``.
9595

96+
.. versionchanged:: 3.12
97+
The setting of ``__cached__``, ``__loader__``, and
98+
``__package__`` are deprecated. See
99+
:class:`~importlib.machinery.ModuleSpec` for alternatives.
100+
96101
.. function:: run_path(path_name, init_globals=None, run_name=None)
97102

98103
.. index::
@@ -163,6 +168,10 @@ The :mod:`runpy` module provides two functions:
163168
case where ``__main__`` is imported from a valid sys.path entry rather
164169
than being executed directly.
165170

171+
.. versionchanged:: 3.12
172+
The setting of ``__cached__``, ``__loader__``, and
173+
``__package__`` are deprecated.
174+
166175
.. seealso::
167176

168177
:pep:`338` -- Executing modules as scripts

Doc/whatsnew/3.12.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,8 @@ Pending Removal in Python 3.14
280280
* Creating :c:data:`immutable types <Py_TPFLAGS_IMMUTABLETYPE>` with mutable
281281
bases using the C API.
282282

283-
* ``__package__`` will cease to be set or taken into consideration by
284-
the import system (:gh:`97879`).
283+
* ``__package__`` and ``__cached__`` will cease to be set or taken
284+
into consideration by the import system (:gh:`97879`).
285285

286286

287287
Pending Removal in Future Versions

Lib/cProfile.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
__all__ = ["run", "runctx", "Profile"]
88

99
import _lsprof
10+
import importlib.machinery
1011
import profile as _pyprofile
1112

1213
# ____________________________________________________________
@@ -169,9 +170,12 @@ def main():
169170
sys.path.insert(0, os.path.dirname(progname))
170171
with open(progname, 'rb') as fp:
171172
code = compile(fp.read(), progname, 'exec')
173+
spec = importlib.machinery.ModuleSpec(name='__main__', loader=None,
174+
origin=progname)
172175
globs = {
173-
'__file__': progname,
174-
'__name__': '__main__',
176+
'__spec__': spec,
177+
'__file__': spec.origin,
178+
'__name__': spec.name,
175179
'__package__': None,
176180
'__cached__': None,
177181
}

Lib/importlib/_bootstrap_external.py

+18-10
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,16 @@ def _path_isabs(path):
182182
return path.startswith(path_separators)
183183

184184

185+
def _path_abspath(path):
186+
"""Replacement for os.path.abspath."""
187+
if not _path_isabs(path):
188+
for sep in path_separators:
189+
path = path.removeprefix(f".{sep}")
190+
return _path_join(_os.getcwd(), path)
191+
else:
192+
return path
193+
194+
185195
def _write_atomic(path, data, mode=0o666):
186196
"""Best-effort function to write data to a path atomically.
187197
Be prepared to handle a FileExistsError if concurrent writing of the
@@ -494,8 +504,7 @@ def cache_from_source(path, debug_override=None, *, optimization=None):
494504
# make it absolute (`C:\Somewhere\Foo\Bar`), then make it root-relative
495505
# (`Somewhere\Foo\Bar`), so we end up placing the bytecode file in an
496506
# unambiguous `C:\Bytecode\Somewhere\Foo\Bar\`.
497-
if not _path_isabs(head):
498-
head = _path_join(_os.getcwd(), head)
507+
head = _path_abspath(head)
499508

500509
# Strip initial drive from a Windows path. We know we have an absolute
501510
# path here, so the second part of the check rules out a POSIX path that
@@ -808,11 +817,10 @@ def spec_from_file_location(name, location=None, *, loader=None,
808817
pass
809818
else:
810819
location = _os.fspath(location)
811-
if not _path_isabs(location):
812-
try:
813-
location = _path_join(_os.getcwd(), location)
814-
except OSError:
815-
pass
820+
try:
821+
location = _path_abspath(location)
822+
except OSError:
823+
pass
816824

817825
# If the location is on the filesystem, but doesn't actually exist,
818826
# we could return None here, indicating that the location is not
@@ -1564,10 +1572,8 @@ def __init__(self, path, *loader_details):
15641572
# Base (directory) path
15651573
if not path or path == '.':
15661574
self.path = _os.getcwd()
1567-
elif not _path_isabs(path):
1568-
self.path = _path_join(_os.getcwd(), path)
15691575
else:
1570-
self.path = path
1576+
self.path = _path_abspath(path)
15711577
self._path_mtime = -1
15721578
self._path_cache = set()
15731579
self._relaxed_path_cache = set()
@@ -1717,6 +1723,8 @@ def _fix_up_module(ns, name, pathname, cpathname=None):
17171723
loader = SourceFileLoader(name, pathname)
17181724
if not spec:
17191725
spec = spec_from_file_location(name, pathname, loader=loader)
1726+
if cpathname:
1727+
spec.cached = _path_abspath(cpathname)
17201728
try:
17211729
ns['__spec__'] = spec
17221730
ns['__loader__'] = loader

Lib/inspect.py

+3-18
Original file line numberDiff line numberDiff line change
@@ -281,30 +281,15 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False):
281281

282282
# ----------------------------------------------------------- type-checking
283283
def ismodule(object):
284-
"""Return true if the object is a module.
285-
286-
Module objects provide these attributes:
287-
__cached__ pathname to byte compiled file
288-
__doc__ documentation string
289-
__file__ filename (missing for built-in modules)"""
284+
"""Return true if the object is a module."""
290285
return isinstance(object, types.ModuleType)
291286

292287
def isclass(object):
293-
"""Return true if the object is a class.
294-
295-
Class objects provide these attributes:
296-
__doc__ documentation string
297-
__module__ name of module in which this class was defined"""
288+
"""Return true if the object is a class."""
298289
return isinstance(object, type)
299290

300291
def ismethod(object):
301-
"""Return true if the object is an instance method.
302-
303-
Instance method objects provide these attributes:
304-
__doc__ documentation string
305-
__name__ name with which this method was defined
306-
__func__ function object containing implementation of method
307-
__self__ instance to which this method is bound"""
292+
"""Return true if the object is an instance method."""
308293
return isinstance(object, types.MethodType)
309294

310295
def ismethoddescriptor(object):

Lib/profile.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
# governing permissions and limitations under the License.
2525

2626

27+
import importlib.machinery
2728
import sys
2829
import time
2930
import marshal
@@ -589,9 +590,12 @@ def main():
589590
sys.path.insert(0, os.path.dirname(progname))
590591
with open(progname, 'rb') as fp:
591592
code = compile(fp.read(), progname, 'exec')
593+
spec = importlib.machinery.ModuleSpec(name='__main__', loader=None,
594+
origin=progname)
592595
globs = {
593-
'__file__': progname,
594-
'__name__': '__main__',
596+
'__spec__': spec,
597+
'__file__': spec.origin,
598+
'__name__': spec.name,
595599
'__package__': None,
596600
'__cached__': None,
597601
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Tests for helper functions used by import.c ."""
2+
3+
from importlib import _bootstrap_external, machinery
4+
import os.path
5+
import unittest
6+
7+
from .. import util
8+
9+
10+
class FixUpModuleTests:
11+
12+
def test_no_loader_but_spec(self):
13+
loader = object()
14+
name = "hello"
15+
path = "hello.py"
16+
spec = machinery.ModuleSpec(name, loader)
17+
ns = {"__spec__": spec}
18+
_bootstrap_external._fix_up_module(ns, name, path)
19+
20+
expected = {"__spec__": spec, "__loader__": loader, "__file__": path,
21+
"__cached__": None}
22+
self.assertEqual(ns, expected)
23+
24+
def test_no_loader_no_spec_but_sourceless(self):
25+
name = "hello"
26+
path = "hello.py"
27+
ns = {}
28+
_bootstrap_external._fix_up_module(ns, name, path, path)
29+
30+
expected = {"__file__": path, "__cached__": path}
31+
32+
for key, val in expected.items():
33+
with self.subTest(f"{key}: {val}"):
34+
self.assertEqual(ns[key], val)
35+
36+
spec = ns["__spec__"]
37+
self.assertIsInstance(spec, machinery.ModuleSpec)
38+
self.assertEqual(spec.name, name)
39+
self.assertEqual(spec.origin, os.path.abspath(path))
40+
self.assertEqual(spec.cached, os.path.abspath(path))
41+
self.assertIsInstance(spec.loader, machinery.SourcelessFileLoader)
42+
self.assertEqual(spec.loader.name, name)
43+
self.assertEqual(spec.loader.path, path)
44+
self.assertEqual(spec.loader, ns["__loader__"])
45+
46+
def test_no_loader_no_spec_but_source(self):
47+
name = "hello"
48+
path = "hello.py"
49+
ns = {}
50+
_bootstrap_external._fix_up_module(ns, name, path)
51+
52+
expected = {"__file__": path, "__cached__": None}
53+
54+
for key, val in expected.items():
55+
with self.subTest(f"{key}: {val}"):
56+
self.assertEqual(ns[key], val)
57+
58+
spec = ns["__spec__"]
59+
self.assertIsInstance(spec, machinery.ModuleSpec)
60+
self.assertEqual(spec.name, name)
61+
self.assertEqual(spec.origin, os.path.abspath(path))
62+
self.assertIsInstance(spec.loader, machinery.SourceFileLoader)
63+
self.assertEqual(spec.loader.name, name)
64+
self.assertEqual(spec.loader.path, path)
65+
self.assertEqual(spec.loader, ns["__loader__"])
66+
67+
68+
FrozenFixUpModuleTests, SourceFixUpModuleTests = util.test_both(FixUpModuleTests)
69+
70+
if __name__ == "__main__":
71+
unittest.main()

Lib/test/test_inspect.py

+3
Original file line numberDiff line numberDiff line change
@@ -4358,8 +4358,11 @@ def test_details(self):
43584358
'unittest', '--details')
43594359
output = out.decode()
43604360
# Just a quick sanity check on the output
4361+
self.assertIn(module.__spec__.name, output)
43614362
self.assertIn(module.__name__, output)
4363+
self.assertIn(module.__spec__.origin, output)
43624364
self.assertIn(module.__file__, output)
4365+
self.assertIn(module.__spec__.cached, output)
43634366
self.assertIn(module.__cached__, output)
43644367
self.assertEqual(err, b'')
43654368

Lib/test/test_pydoc.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,7 @@ def test_synopsis(self):
702702
def test_synopsis_sourceless(self):
703703
os = import_helper.import_fresh_module('os')
704704
expected = os.__doc__.splitlines()[0]
705-
filename = os.__cached__
705+
filename = os.__spec__.cached
706706
synopsis = pydoc.synopsis(filename)
707707

708708
self.assertEqual(synopsis, expected)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Do not rely solely on ``__cached__`` on modules; code will also support
2+
``__spec__.cached``.

0 commit comments

Comments
 (0)