Skip to content

Commit 631a76c

Browse files
authored
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
1 parent 86018d3 commit 631a76c

File tree

5 files changed

+65
-19
lines changed

5 files changed

+65
-19
lines changed

ChangeLog

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ What's New in astroid 3.3.7?
3333
============================
3434
Release date: TBA
3535

36-
* Fix inability to import `collections.abc` in python 3.13.1.
36+
* Fix inability to import `collections.abc` in python 3.13.1. The reported fix in astroid 3.3.6
37+
did not actually fix this issue.
3738

3839
Closes pylint-dev/pylint#10112
3940

@@ -43,6 +44,7 @@ What's New in astroid 3.3.6?
4344
Release date: 2024-12-08
4445

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

4749
Closes pylint-dev/pylint#10112
4850

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

165195
suffixes = (".py", ".pyi", importlib.machinery.BYTECODE_SUFFIXES[0])

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)