Repository: cython/cython
Pull request: Fix attribute lookup of standard library names
The attributes were accidently being converted to NameNode instead. The tests weren't picking this up because the names were available so were being looked up successfully.

Fixes #5536

Problem statement:
[BUG] NameError: name 'Dict' is not defined
### Describe the bug

When import `typing` and accessing `typing.Dict` a `NameError` is raised.

This does not occur with `Cython<3`.

### Code to reproduce the behaviour:

```cython
import typing
typing.Dict
```

### Expected behaviour

An exception is not raised.

### OS

macOS, Linux

### Python version

3.10.8

### Cython version

3.0.0

### Additional context

_No response_

Hints from issue discussion:
Hmm. I'm fairly sure we test more complicated versions of that so I wonder what's going wrong with the simple version. Thanks for the report
The bug occurs for us in a "more complex" scenario, but I was able to reproduce with this simple example.

If it helps to have our actual source of the issue for context: https://github.com/DataDog/dd-trace-py/blob/7f73c55c66fc8201dda021fe2fec2d812feb3d06/ddtrace/profiling/_threading.pyx#L103
Definitely seems like it is there, so curious why attribute access doesn't work 🤷🏻‍♂️ 

--- 

```cython
import typing
print(dir(typing))
```

```
[..., 'Dict', ...]
```
---

```cython
import typing
print(getattr(typing, "Dict"))
```

```
typing.Dict
```

Commit message: Fix attribute lookup of standard library names

The attributes were accidently being converted to NameNode
instead. The tests weren't picking this up because the names
were available so were being looked up successfully.

Fixes #5536

Patch to review:
diff --git a/Cython/Compiler/Builtin.py b/Cython/Compiler/Builtin.py
index f7540a6084b..0559a14ff9a 100644
--- a/Cython/Compiler/Builtin.py
+++ b/Cython/Compiler/Builtin.py
@@ -572,6 +572,7 @@ def get_known_standard_library_module_scope(module_name):
             var_entry.is_variable = True
             var_entry.scope = mod
             entry.as_variable = var_entry
+            entry.known_standard_library_import = "%s.%s" % (module_name, name)
 
         for name in ['ClassVar', 'Optional']:
             name = EncodedString(name)
@@ -582,6 +583,7 @@ def get_known_standard_library_module_scope(module_name):
             var_entry.is_variable = True
             var_entry.scope = mod
             entry.as_variable = var_entry
+            entry.known_standard_library_import = "%s.%s" % (module_name, name)
         _known_module_scopes[module_name] = mod
     elif module_name == "dataclasses":
         mod = ModuleScope(module_name, None, None)
@@ -592,6 +594,7 @@ def get_known_standard_library_module_scope(module_name):
         var_entry.is_pyglobal = True
         var_entry.scope = mod
         entry.as_variable = var_entry
+        entry.known_standard_library_import = "%s.%s" % (module_name, name)
         for name in ["dataclass", "field"]:
             mod.declare_var(EncodedString(name), PyrexTypes.py_object_type, pos=None)
         _known_module_scopes[module_name] = mod
diff --git a/Cython/Compiler/ExprNodes.py b/Cython/Compiler/ExprNodes.py
index 275a6233da5..dd71d528c97 100644
--- a/Cython/Compiler/ExprNodes.py
+++ b/Cython/Compiler/ExprNodes.py
@@ -7341,7 +7341,7 @@ def analyse_as_cimported_attribute_node(self, env, target):
         module_scope = self.obj.analyse_as_module(env)
         if module_scope:
             entry = module_scope.lookup_here(self.attribute)
-            if entry and (
+            if entry and not entry.known_standard_library_import and (
                     entry.is_cglobal or entry.is_cfunction
                     or entry.is_type or entry.is_const):
                 return self.as_name_node(env, entry, target)
diff --git a/tests/run/pep526_variable_annotations.py b/tests/run/pep526_variable_annotations.py
index 05d8c60b1a8..ea22157e127 100644
--- a/tests/run/pep526_variable_annotations.py
+++ b/tests/run/pep526_variable_annotations.py
@@ -8,6 +8,8 @@
 
 import cython
 
+# Don't add FrozenSet to this list - it's necessary for one of the tests
+# that it isn't a module global name
 from typing import Dict, List, TypeVar, Optional, Generic, Tuple
 
 try:
@@ -267,18 +269,24 @@ def test_use_typing_attributes_as_non_annotations():
     x1 = typing.Tuple
     x2 = typing.Tuple[int]
     y1 = typing.Optional
-    y2 = typing.Optional[typing.Dict]
+    # It's important for the test that FrozenSet isn't available in the module namespace,
+    # since one bug would have looked it up there rather than as an attribute of typing
+    y2 = typing.Optional[typing.FrozenSet]
     z1 = Optional
     z2 = Optional[Dict]
     # The result of printing "Optional[type]" is slightly version-dependent
     # so accept both possible forms
-    allowed_optional_strings = [
+    allowed_optional_frozenset_strings = [
+        "typing.Union[typing.FrozenSet, NoneType]",
+        "typing.Optional[typing.FrozenSet]"
+    ]
+    allowed_optional_dict_strings = [
         "typing.Union[typing.Dict, NoneType]",
         "typing.Optional[typing.Dict]"
     ]
     print(x1, x2)
-    print(y1, str(y2) in allowed_optional_strings)
-    print(z1, str(z2) in allowed_optional_strings)
+    print(y1, str(y2) in allowed_optional_frozenset_strings)
+    print(z1, str(z2) in allowed_optional_dict_strings)
 
 def test_optional_ctuple(x: typing.Optional[tuple[float]]):
     """


Code review comments:
diff_hunk: @@ -592,6 +594,7 @@ def get_known_standard_library_module_scope(module_name):
         var_entry.is_pyglobal = True
         var_entry.scope = mod
         entry.as_variable = var_entry
+        entry.known_standard_library_import = "%s.%s" % (module_name, name)
path: Cython/Compiler/Builtin.py
text: @user1:
For the typing module, `name = EncodedString(name)` for whichever attribute of the module it is. For dataclasses we don't have that.

@author:
```suggestion
        entry.known_standard_library_import = "%s.InitVar" % module_name
```