diff --git a/astroid/brain/brain_gi.py b/astroid/brain/brain_gi.py index 4ebbdde2ab..b7b6177acd 100644 --- a/astroid/brain/brain_gi.py +++ b/astroid/brain/brain_gi.py @@ -245,6 +245,6 @@ def _register_require_version(node): def register(manager: AstroidManager) -> None: manager.register_failed_import_hook(_import_gi_module) - manager.register_transform( + manager.register_early_transform( nodes.Call, _register_require_version, _looks_like_require_version ) diff --git a/astroid/builder.py b/astroid/builder.py index cff859124e..6b012f2fbd 100644 --- a/astroid/builder.py +++ b/astroid/builder.py @@ -102,6 +102,7 @@ def module_build( if self._apply_transforms: # We have to handle transformation by ourselves since the # rebuilder isn't called for builtin nodes + node = self._manager.visit_early_transforms(node) node = self._manager.visit_transforms(node) assert isinstance(node, nodes.Module) return node @@ -164,6 +165,10 @@ def _post_build( for symbol, _ in from_node.names: module.future_imports.add(symbol) self.add_from_names_to_locals(from_node) + # Visit the transforms + if self._apply_transforms: + module = self._manager.visit_early_transforms(module) + # handle delayed assattr nodes for delayed in builder._delayed_assattr: self.delayed_assattr(delayed) diff --git a/astroid/manager.py b/astroid/manager.py index e7c2c806f7..47b6b7423e 100644 --- a/astroid/manager.py +++ b/astroid/manager.py @@ -62,6 +62,7 @@ class AstroidManager: "extension_package_whitelist": set(), "module_denylist": set(), "_transform": TransformVisitor(), + "_early_transform": TransformVisitor(), "prefer_stubs": False, } @@ -75,6 +76,7 @@ def __init__(self) -> None: ] self.module_denylist = AstroidManager.brain["module_denylist"] self._transform = AstroidManager.brain["_transform"] + self._early_transform = AstroidManager.brain["_early_transform"] self.prefer_stubs = AstroidManager.brain["prefer_stubs"] @property @@ -110,6 +112,15 @@ def register_transform(self): def unregister_transform(self): return self._transform.unregister_transform + @property + def register_early_transform(self): + # This and unregister_early_transform below are exported for convenience + return self._early_transform.register_transform + + @property + def unregister_early_transform(self): + return self._early_transform.unregister_transform + @property def builtins_module(self) -> nodes.Module: return self.astroid_cache["builtins"] @@ -126,6 +137,10 @@ def visit_transforms(self, node: nodes.NodeNG) -> InferenceResult: """Visit the transforms and apply them to the given *node*.""" return self._transform.visit(node) + def visit_early_transforms(self, node: nodes.NodeNG) -> InferenceResult: + """Visit the early transforms and apply them to the given *node*.""" + return self._early_transform.visit(node) + def ast_from_file( self, filepath: str, @@ -466,6 +481,9 @@ def clear_cache(self) -> None: self.astroid_cache.clear() # NB: not a new TransformVisitor() AstroidManager.brain["_transform"].transforms = collections.defaultdict(list) + AstroidManager.brain["_early_transform"].transforms = collections.defaultdict( + list + ) for lru_cache in ( LookupMixIn.lookup, diff --git a/astroid/test_utils.py b/astroid/test_utils.py index afddb1a4ff..4937620b4f 100644 --- a/astroid/test_utils.py +++ b/astroid/test_utils.py @@ -73,6 +73,7 @@ def brainless_manager(): m.astroid_cache = {} m._mod_file_cache = {} m._transform = transforms.TransformVisitor() + m._early_transform = transforms.TransformVisitor() m.extension_package_whitelist = set() m.module_denylist = set() return m diff --git a/tests/brain/test_gi.py b/tests/brain/test_gi.py new file mode 100644 index 0000000000..25f99da1ed --- /dev/null +++ b/tests/brain/test_gi.py @@ -0,0 +1,41 @@ +# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html +# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE +# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt + +import warnings +from importlib.util import find_spec + +import pytest + +from astroid import Uninferable, extract_node +from astroid.bases import BoundMethod +from astroid.manager import AstroidManager + +HAS_GI = find_spec("gi") + + +@pytest.mark.skipif(HAS_GI is None, reason="These tests require the gi library.") +class TestBrainGi: + AstroidManager.brain["extension_package_whitelist"] = {"gi"} # noqa: RUF012 + + @staticmethod + def test_import() -> None: + """Regression test for https://github.com/pylint-dev/astroid/issues/2190""" + src = """ + import gi + gi.require_version('Gtk', '3.0') + from gi.repository import Gtk + + cell = Gtk.CellRendererText() + cell.props.xalign = 1.0 + + Gtk.Builder().connect_signals + """ + with warnings.catch_warnings(): + # gi uses pkgutil.get_loader + warnings.filterwarnings("ignore", category=DeprecationWarning) + node = extract_node(src) + attribute_node = node.inferred()[0] + if attribute_node is Uninferable: + pytest.skip("Gtk3 may not be installed?") + assert isinstance(attribute_node, BoundMethod) diff --git a/tests/test_builder.py b/tests/test_builder.py index 8692baabae..b94b8edcfc 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -487,6 +487,22 @@ def transform_time(node: Module) -> None: finally: self.manager.unregister_transform(nodes.Module, transform_time) + def test_inspect_early_transform_module(self) -> None: + # ensure no cached version of the time module + self.manager._mod_file_cache.pop(("time", None), None) + self.manager.astroid_cache.pop("time", None) + + def transform_time(node: Module) -> None: + if node.name == "time": + node.transformed = True + + self.manager.register_early_transform(nodes.Module, transform_time) + try: + time_ast = self.manager.ast_from_module_name("time") + self.assertTrue(getattr(time_ast, "transformed", False)) + finally: + self.manager.unregister_early_transform(nodes.Module, transform_time) + def test_package_name(self) -> None: """Test base properties and method of an astroid module.""" datap = resources.build_file("data/__init__.py", "data")