Repository: serge-sans-paille/beniget

Issue:
Some builtins are only available under Windows or newer Python version
The [WindowsError](https://docs.python.org/3.14/library/exceptions.html#WindowsError) builtin name only exist under Windows, so if we are analyzing a module that uses it under Linux it will erroneously marked as unbound. 

A possible solution would be to use typeshed to parse the builtins instead of using the actual runtime values. EDIT: This would be too much time consuming… we would have to cache it globally and that probably not a good practice 

Another solution would be to manually add the missing names. But it will need maintenance if new problematic builtin names are discovered.

Hints from issue comments:
@tristanlatr: The aiter and anext are only available under Python 3.10+.
EncodingWarning as well, and PythonFinalizationError is for Python 3.13+. So the same kind of problem applies to Python version as well.@tristanlatr: Another idea: maybe the builtin dict can be passed as a parameter so this issue can be fixed in the client side ?

Solution patch:
diff --git a/beniget/beniget.py b/beniget/beniget.py
index e7714d3..625b45b 100644
--- a/beniget/beniget.py
+++ b/beniget/beniget.py
@@ -202,6 +202,15 @@ def _str(self, nodes):
 # this should probably be assigned to the filename give to DefUseChains instead.
 Builtins["__file__"] = __file__
 
+# Cope with conditionally existing builtins by special-casing them.
+Builtins.setdefault('WindowsError', object()) # never defined under Linux
+Builtins.setdefault('anext', object()) # added in Python 3.10
+Builtins.setdefault('aiter', object()) # added in Python 3.10
+Builtins.setdefault('EncodingWarning', object()) # added in Python 3.10
+Builtins.setdefault('PythonFinalizationError', object()) # added in Python 3.13
+# beniget doesn't run Python 3.5 and below, so we don't need to 
+# account for names introduced before Python 3.6
+
 DeclarationStep, DefinitionStep = object(), object()
 
 def collect_future_imports(node):


Test patch:
diff --git a/tests/test_chains.py b/tests/test_chains.py
index 396f66d..db423c2 100644
--- a/tests/test_chains.py
+++ b/tests/test_chains.py
@@ -1311,10 +1311,23 @@ class mytype2(int):
         # to the inner classes.
 
     def test_lookup_scopes(self):
-        defuse = beniget.DefUseChains()
-        defuse.visit(self.ast.Module(body=[]))
+        
+        def get_scopes():
+            yield self.ast.parse('')                                # Module
+            yield self.ast.parse('def f(): pass').body[0]           # FunctionDef
+            yield self.ast.parse('class C: pass').body[0]           # ClassDef
+            yield self.ast.parse('lambda: True').body[0].value      # Lambda
+            yield self.ast.parse('(x for x in list())').body[0].value  # GeneratorExp
+            yield self.ast.parse('{k:v for k, v in dict().items()}').body[0].value  # DictComp
+
+        mod, fn, cls, lambd, gen, comp = get_scopes()
+        assert isinstance(mod, self.ast.Module)
+        assert isinstance(fn, self.ast.FunctionDef)
+        assert isinstance(cls, self.ast.ClassDef)
+        assert isinstance(lambd, self.ast.Lambda)
+        assert isinstance(gen, self.ast.GeneratorExp)
+        assert isinstance(comp, self.ast.DictComp)
 
-        mod, fn, cls, lambd, gen, comp = self.ast.Module(), self.ast.FunctionDef(), self.ast.ClassDef(), self.ast.Lambda(), self.ast.GeneratorExp(), self.ast.DictComp()
         assert _get_lookup_scopes((mod, fn, fn, fn, cls)) == [mod, fn, fn, fn, cls]
         assert _get_lookup_scopes((mod, fn, fn, fn, cls, fn)) == [mod, fn, fn, fn, fn]
         assert _get_lookup_scopes((mod, cls, fn)) == [mod, fn]
@@ -1548,6 +1561,17 @@ class Point:
                        'x -> (<MatchClass> -> (), x -> (<Call> -> ()))',
                        'y -> (<MatchClass> -> (), y -> (<Call> -> ()))'])
 
+    def test_WindowsError_builtin_name(self):
+        # Tests for issue https://github.com/serge-sans-paille/beniget/issues/119
+        code = 'try: 1/0\nexcept WindowsError as e: raise'
+        self.check_message(code, [])
+    
+    def test_newer_Python_version_builtin_name(self):
+        # Tests for issue https://github.com/serge-sans-paille/beniget/issues/119
+        code = ('try: 1/0\nexcept (PythonFinalizationError, EncodingWarning) as e: raise\n'
+                'a,b = anext(), aiter()')
+        self.check_message(code, [])
+
 
 class TestDefUseChainsStdlib(TestDefUseChains):
     ast = _ast