Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit e1c4d56

Browse filesBrowse 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
Copy full SHA for e1c4d56

File tree

11 files changed

+130
-35
lines changed
Filter options

11 files changed

+130
-35
lines changed

‎Doc/c-api/import.rst

Copy file name to clipboardExpand all lines: Doc/c-api/import.rst
+9Lines changed: 9 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: Doc/library/runpy.rst
+9Lines changed: 9 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: Doc/whatsnew/3.12.rst
+2-2Lines changed: 2 additions & 2 deletions
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

Copy file name to clipboardExpand all lines: Lib/cProfile.py
+6-2Lines changed: 6 additions & 2 deletions
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

Copy file name to clipboardExpand all lines: Lib/importlib/_bootstrap_external.py
+18-10Lines changed: 18 additions & 10 deletions
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

Copy file name to clipboardExpand all lines: Lib/inspect.py
+3-18Lines changed: 3 additions & 18 deletions
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

Copy file name to clipboardExpand all lines: Lib/profile.py
+6-2Lines changed: 6 additions & 2 deletions
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
}
+71Lines changed: 71 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: Lib/test/test_inspect.py
+3Lines changed: 3 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: Lib/test/test_pydoc.py
+1-1Lines changed: 1 addition & 1 deletion
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)
+2Lines changed: 2 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.