Adjust `is_namespace()` to check `ModuleSpec.loader` (#2410)
This fixes inference when six.moves is imported.
Closes #2409
diff --git a/ChangeLog b/ChangeLog
index c611984..5e4f68f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -19,6 +19,13 @@
Refs pylint-dev/pylint#9442
+* Make ``astroid.interpreter._import.util.is_namespace`` only consider modules
+ using a loader set to ``NamespaceLoader`` or ``None`` as namespaces.
+ This fixes a problem that ``six.moves`` brain was not effective if ``six.moves``
+ was already imported.
+
+ Closes #1107
+
What's New in astroid 3.1.1?
============================
diff --git a/astroid/interpreter/_import/util.py b/astroid/interpreter/_import/util.py
index a8af9ec..511ec4f 100644
--- a/astroid/interpreter/_import/util.py
+++ b/astroid/interpreter/_import/util.py
@@ -12,6 +12,11 @@
from astroid.const import IS_PYPY
+if sys.version_info >= (3, 11):
+ from importlib.machinery import NamespaceLoader
+else:
+ from importlib._bootstrap_external import _NamespaceLoader as NamespaceLoader
+
@lru_cache(maxsize=4096)
def is_namespace(modname: str) -> bool:
@@ -101,4 +106,7 @@
found_spec is not None
and found_spec.submodule_search_locations is not None
and found_spec.origin is None
+ and (
+ found_spec.loader is None or isinstance(found_spec.loader, NamespaceLoader)
+ )
)
diff --git a/tests/brain/test_six.py b/tests/brain/test_six.py
index c7c9db7..45a40e5 100644
--- a/tests/brain/test_six.py
+++ b/tests/brain/test_six.py
@@ -29,6 +29,8 @@
six.moves.urllib_parse #@
six.moves.urllib_error #@
six.moves.urllib.request #@
+ from six.moves import StringIO
+ StringIO #@
"""
)
assert isinstance(ast_nodes, list)
@@ -64,6 +66,18 @@
self.assertIsInstance(urlretrieve, nodes.FunctionDef)
self.assertEqual(urlretrieve.qname(), "urllib.request.urlretrieve")
+ StringIO = next(ast_nodes[4].infer())
+ self.assertIsInstance(StringIO, nodes.ClassDef)
+ self.assertEqual(StringIO.qname(), "_io.StringIO")
+ self.assertTrue(StringIO.callable())
+
+ def test_attribute_access_with_six_moves_imported(self) -> None:
+ astroid.MANAGER.clear_cache()
+ astroid.MANAGER._mod_file_cache.clear()
+ import six.moves # type: ignore[import] # pylint: disable=import-outside-toplevel,unused-import,redefined-outer-name
+
+ self.test_attribute_access()
+
def test_from_imports(self) -> None:
ast_node = builder.extract_node(
"""