Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix collections.abc imports on Python 3.13.0 and 3.13.1 #2657

Merged
merged 3 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ What's New in astroid 3.3.7?
============================
Release date: TBA

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

Closes pylint-dev/pylint#10112

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

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

Closes pylint-dev/pylint#10112

Expand Down
11 changes: 5 additions & 6 deletions astroid/brain/brain_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from astroid.brain.helpers import register_module_extender
from astroid.builder import AstroidBuilder, extract_node, parse
from astroid.const import PY313_0, PY313_PLUS
from astroid.const import PY313_PLUS
from astroid.context import InferenceContext
from astroid.exceptions import AttributeInferenceError
from astroid.manager import AstroidManager
Expand All @@ -20,8 +20,7 @@

def _collections_transform():
return parse(
(" import _collections_abc as abc" if PY313_PLUS and not PY313_0 else "")
+ """
"""
class defaultdict(dict):
default_factory = None
def __missing__(self, key): pass
Expand All @@ -33,7 +32,7 @@ def __getitem__(self, key): return default_factory
)


def _collections_abc_313_0_transform() -> nodes.Module:
def _collections_abc_313_transform() -> nodes.Module:
"""See https://github.com/python/cpython/pull/124735"""
return AstroidBuilder(AstroidManager()).string_build(
"from _collections_abc import *"
Expand Down Expand Up @@ -133,7 +132,7 @@ def register(manager: AstroidManager) -> None:
ClassDef, easy_class_getitem_inference, _looks_like_subscriptable
)

if PY313_0:
if PY313_PLUS:
register_module_extender(
manager, "collections.abc", _collections_abc_313_0_transform
manager, "collections.abc", _collections_abc_313_transform
)
1 change: 0 additions & 1 deletion astroid/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
PY311_PLUS = sys.version_info >= (3, 11)
PY312_PLUS = sys.version_info >= (3, 12)
PY313_PLUS = sys.version_info >= (3, 13)
PY313_0 = sys.version_info[:3] == (3, 13, 0)

WIN32 = sys.platform == "win32"

Expand Down
52 changes: 41 additions & 11 deletions astroid/interpreter/_import/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import types
import warnings
import zipimport
from collections.abc import Iterable, Iterator, Sequence

Check failure on line 18 in astroid/interpreter/_import/spec.py

View workflow job for this annotation

GitHub Actions / Checks

E0401

Unable to import 'collections.abc'
from functools import lru_cache
from pathlib import Path
from typing import Literal, NamedTuple, Protocol
Expand Down Expand Up @@ -134,32 +134,62 @@
processed: tuple[str, ...],
submodule_path: tuple[str, ...] | None,
) -> ModuleSpec | None:
if submodule_path is not None:
search_paths = list(submodule_path)
elif modname in sys.builtin_module_names:
# Although we should be able to use `find_spec` this doesn't work on PyPy for builtins.
# Therefore, we use the `builtin_module_nams` heuristic for these.
if submodule_path is None and modname in sys.builtin_module_names:
return ModuleSpec(
name=modname,
location=None,
type=ModuleType.C_BUILTIN,
)
else:
try:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=UserWarning)
spec = importlib.util.find_spec(modname)

# sys.stdlib_module_names was added in Python 3.10
if PY310_PLUS:
# If the module is a stdlib module, check whether this is a frozen module. Note that
# `find_spec` actually imports the module, so we want to make sure we only run this code
# for stuff that can be expected to be frozen. For now this is only stdlib.
if modname in sys.stdlib_module_names or (
processed and processed[0] in sys.stdlib_module_names
):
spec = importlib.util.find_spec(".".join((*processed, modname)))
if (
spec
and spec.loader # type: ignore[comparison-overlap] # noqa: E501
is importlib.machinery.FrozenImporter
):
# No need for BuiltinImporter; builtins handled above
return ModuleSpec(
name=modname,
location=getattr(spec.loader_state, "filename", None),
type=ModuleType.PY_FROZEN,
)
except ValueError:
pass
else:
# NOTE: This is broken code. It doesn't work on Python 3.13+ where submodules can also
# be frozen. However, we don't want to worry about this and we don't want to break
# support for older versions of Python. This is just copy-pasted from the old non
# working version to at least have no functional behaviour change on <=3.10.
# It can be removed after 3.10 is no longer supported in favour of the logic above.
if submodule_path is None: # pylint: disable=else-if-used
try:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=UserWarning)
spec = importlib.util.find_spec(modname)
if (
spec
and spec.loader # type: ignore[comparison-overlap] # noqa: E501
is importlib.machinery.FrozenImporter
):
# No need for BuiltinImporter; builtins handled above
return ModuleSpec(
name=modname,
location=getattr(spec.loader_state, "filename", None),
type=ModuleType.PY_FROZEN,
)
except ValueError:
pass

if submodule_path is not None:
search_paths = list(submodule_path)
else:
search_paths = sys.path

suffixes = (".py", ".pyi", importlib.machinery.BYTECODE_SUFFIXES[0])
Expand Down
16 changes: 16 additions & 0 deletions tests/brain/test_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,22 @@ def check_metaclass_is_abc(node: nodes.ClassDef):


class CollectionsBrain(unittest.TestCase):
def test_collections_abc_is_importable(self) -> None:
"""
Test that we can import `collections.abc`.

The collections.abc has gone through various formats of being frozen. Therefore, we ensure
that we can still import it (correctly).
"""
import_node = builder.extract_node("import collections.abc")
assert isinstance(import_node, nodes.Import)
imported_module = import_node.do_import_module(import_node.names[0][0])
# Make sure that the file we have imported is actually the submodule of collections and
# not the `abc` module. (Which would happen if you call `importlib.util.find_spec("abc")`
# instead of `importlib.util.find_spec("collections.abc")`)
assert isinstance(imported_module.file, str)
assert "collections" in imported_module.file

def test_collections_object_not_subscriptable(self) -> None:
"""
Test that unsubscriptable types are detected
Expand Down
Loading