Skip to content

Commit c47a798

Browse files
Pierre-SassoulasDanielNoordcorrectmost
authored
Fix collections.abc imports on Python 3.13.0 and 3.13.1 (#2657) (#2658)
* Fix `collections.abc` imports on Python 3.13.0 and 3.13.1 (#2657) * Add test for importing `collections.abc` * Fix issue with importing of frozen submodules * Fix `search_paths` * Do not reassign submodule_path parameters in method bodies This makes it easier to use less generic annotations with mypy. --------- Co-authored-by: Daniël van Noord <[email protected]> Co-authored-by: correctmost <[email protected]>
1 parent a132679 commit c47a798

File tree

5 files changed

+76
-26
lines changed

5 files changed

+76
-26
lines changed

ChangeLog

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,18 @@ What's New in astroid 3.3.7?
1313
============================
1414
Release date: TBA
1515

16+
* Fix inability to import `collections.abc` in python 3.13.1. The reported fix in astroid 3.3.6
17+
did not actually fix this issue.
18+
19+
Closes pylint-dev/pylint#10112
1620

1721

1822
What's New in astroid 3.3.6?
1923
============================
2024
Release date: 2024-12-08
2125

2226
* Fix inability to import `collections.abc` in python 3.13.1.
27+
_It was later found that this did not resolve the linked issue. It was fixed in astroid 3.3.7_
2328

2429
Closes pylint-dev/pylint#10112
2530

astroid/brain/brain_collections.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from astroid.brain.helpers import register_module_extender
1010
from astroid.builder import AstroidBuilder, extract_node, parse
11-
from astroid.const import PY313_0, PY313_PLUS
11+
from astroid.const import PY313_PLUS
1212
from astroid.context import InferenceContext
1313
from astroid.exceptions import AttributeInferenceError
1414
from astroid.manager import AstroidManager
@@ -20,8 +20,7 @@
2020

2121
def _collections_transform():
2222
return parse(
23-
(" import _collections_abc as abc" if PY313_PLUS and not PY313_0 else "")
24-
+ """
23+
"""
2524
class defaultdict(dict):
2625
default_factory = None
2726
def __missing__(self, key): pass
@@ -33,7 +32,7 @@ def __getitem__(self, key): return default_factory
3332
)
3433

3534

36-
def _collections_abc_313_0_transform() -> nodes.Module:
35+
def _collections_abc_313_transform() -> nodes.Module:
3736
"""See https://github.com/python/cpython/pull/124735"""
3837
return AstroidBuilder(AstroidManager()).string_build(
3938
"from _collections_abc import *"
@@ -133,7 +132,7 @@ def register(manager: AstroidManager) -> None:
133132
ClassDef, easy_class_getitem_inference, _looks_like_subscriptable
134133
)
135134

136-
if PY313_0:
135+
if PY313_PLUS:
137136
register_module_extender(
138-
manager, "collections.abc", _collections_abc_313_0_transform
137+
manager, "collections.abc", _collections_abc_313_transform
139138
)

astroid/const.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
PY311_PLUS = sys.version_info >= (3, 11)
1010
PY312_PLUS = sys.version_info >= (3, 12)
1111
PY313_PLUS = sys.version_info >= (3, 13)
12-
PY313_0 = sys.version_info[:3] == (3, 13, 0)
1312

1413
WIN32 = sys.platform == "win32"
1514

astroid/interpreter/_import/spec.py

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -133,36 +133,66 @@ def find_module(
133133
processed: list[str],
134134
submodule_path: Sequence[str] | None,
135135
) -> ModuleSpec | None:
136-
if submodule_path is not None:
137-
submodule_path = list(submodule_path)
138-
elif modname in sys.builtin_module_names:
136+
# Although we should be able to use `find_spec` this doesn't work on PyPy for builtins.
137+
# Therefore, we use the `builtin_module_nams` heuristic for these.
138+
if submodule_path is None and modname in sys.builtin_module_names:
139139
return ModuleSpec(
140140
name=modname,
141141
location=None,
142142
type=ModuleType.C_BUILTIN,
143143
)
144-
else:
145-
try:
146-
with warnings.catch_warnings():
147-
warnings.filterwarnings("ignore", category=UserWarning)
148-
spec = importlib.util.find_spec(modname)
144+
145+
# sys.stdlib_module_names was added in Python 3.10
146+
if PY310_PLUS:
147+
# If the module is a stdlib module, check whether this is a frozen module. Note that
148+
# `find_spec` actually imports the module, so we want to make sure we only run this code
149+
# for stuff that can be expected to be frozen. For now this is only stdlib.
150+
if modname in sys.stdlib_module_names or (
151+
processed and processed[0] in sys.stdlib_module_names
152+
):
153+
spec = importlib.util.find_spec(".".join((*processed, modname)))
149154
if (
150155
spec
151156
and spec.loader # type: ignore[comparison-overlap] # noqa: E501
152157
is importlib.machinery.FrozenImporter
153158
):
154-
# No need for BuiltinImporter; builtins handled above
155159
return ModuleSpec(
156160
name=modname,
157161
location=getattr(spec.loader_state, "filename", None),
158162
type=ModuleType.PY_FROZEN,
159163
)
160-
except ValueError:
161-
pass
162-
submodule_path = sys.path
164+
else:
165+
# NOTE: This is broken code. It doesn't work on Python 3.13+ where submodules can also
166+
# be frozen. However, we don't want to worry about this and we don't want to break
167+
# support for older versions of Python. This is just copy-pasted from the old non
168+
# working version to at least have no functional behaviour change on <=3.10.
169+
# It can be removed after 3.10 is no longer supported in favour of the logic above.
170+
if submodule_path is None: # pylint: disable=else-if-used
171+
try:
172+
with warnings.catch_warnings():
173+
warnings.filterwarnings("ignore", category=UserWarning)
174+
spec = importlib.util.find_spec(modname)
175+
if (
176+
spec
177+
and spec.loader # type: ignore[comparison-overlap] # noqa: E501
178+
is importlib.machinery.FrozenImporter
179+
):
180+
# No need for BuiltinImporter; builtins handled above
181+
return ModuleSpec(
182+
name=modname,
183+
location=getattr(spec.loader_state, "filename", None),
184+
type=ModuleType.PY_FROZEN,
185+
)
186+
except ValueError:
187+
pass
188+
189+
if submodule_path is not None:
190+
search_paths = list(submodule_path)
191+
else:
192+
search_paths = sys.path
163193

164194
suffixes = (".py", ".pyi", importlib.machinery.BYTECODE_SUFFIXES[0])
165-
for entry in submodule_path:
195+
for entry in search_paths:
166196
package_directory = os.path.join(entry, modname)
167197
for suffix in suffixes:
168198
package_file_name = "__init__" + suffix
@@ -231,13 +261,12 @@ def find_module(
231261
if processed:
232262
modname = ".".join([*processed, modname])
233263
if util.is_namespace(modname) and modname in sys.modules:
234-
submodule_path = sys.modules[modname].__path__
235264
return ModuleSpec(
236265
name=modname,
237266
location="",
238267
origin="namespace",
239268
type=ModuleType.PY_NAMESPACE,
240-
submodule_search_locations=submodule_path,
269+
submodule_search_locations=sys.modules[modname].__path__,
241270
)
242271
return None
243272

@@ -353,13 +382,15 @@ def _search_zip(
353382
if PY310_PLUS:
354383
if not importer.find_spec(os.path.sep.join(modpath)):
355384
raise ImportError(
356-
"No module named %s in %s/%s"
357-
% (".".join(modpath[1:]), filepath, modpath)
385+
"No module named {} in {}/{}".format(
386+
".".join(modpath[1:]), filepath, modpath
387+
)
358388
)
359389
elif not importer.find_module(os.path.sep.join(modpath)):
360390
raise ImportError(
361-
"No module named %s in %s/%s"
362-
% (".".join(modpath[1:]), filepath, modpath)
391+
"No module named {} in {}/{}".format(
392+
".".join(modpath[1:]), filepath, modpath
393+
)
363394
)
364395
return (
365396
ModuleType.PY_ZIPMODULE,

tests/brain/test_brain.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,22 @@ def check_metaclass_is_abc(node: nodes.ClassDef):
192192

193193

194194
class CollectionsBrain(unittest.TestCase):
195+
def test_collections_abc_is_importable(self) -> None:
196+
"""
197+
Test that we can import `collections.abc`.
198+
199+
The collections.abc has gone through various formats of being frozen. Therefore, we ensure
200+
that we can still import it (correctly).
201+
"""
202+
import_node = builder.extract_node("import collections.abc")
203+
assert isinstance(import_node, nodes.Import)
204+
imported_module = import_node.do_import_module(import_node.names[0][0])
205+
# Make sure that the file we have imported is actually the submodule of collections and
206+
# not the `abc` module. (Which would happen if you call `importlib.util.find_spec("abc")`
207+
# instead of `importlib.util.find_spec("collections.abc")`)
208+
assert isinstance(imported_module.file, str)
209+
assert "collections" in imported_module.file
210+
195211
def test_collections_object_not_subscriptable(self) -> None:
196212
"""
197213
Test that unsubscriptable types are detected

0 commit comments

Comments
 (0)