From 9518cd4b1917a712704cb5324f21a99f002ad5d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Mon, 24 Jun 2024 02:41:09 +0200 Subject: [PATCH] Add early transforms before handling delayed assattr nodes When importing Gtk, it looks like this: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk It is vital that gi.require_version() is made before related 'from gi.repository import ...'. The brain_gi tries to do that using transforms. And it works unless Gtk is imported as part of delayed assattr handling. Fix this by adding early transforms that are called before delayed assattr. Fixes https://github.com/pylint-dev/astroid/issues/2190 Fixes https://github.com/pylint-dev/pylint/issues/6352 --- astroid/brain/brain_gi.py | 2 +- astroid/builder.py | 5 +++++ astroid/manager.py | 18 ++++++++++++++++++ astroid/test_utils.py | 1 + tests/test_builder.py | 16 ++++++++++++++++ 5 files changed, 41 insertions(+), 1 deletion(-) 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/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")