Repository: python/mypy

Issue:
__spec__ incorrectly flagged as not existing in the global/module namespace
If you have code in a module that wants to access a module's `__spec__` attribute through the global namespace, mypy claims that `error: Name '__spec__' is not defined`. E.g. `@unittest.skipIf(__spec__ is None, '__spec__ is None')`. This same issue doesn't come up for `__name__` or `__path__`.


Discussion:
It's probably just missing __spec__ in the typeshed stub for ModuleType
in types.pyi.

I think this is similar to https://github.com/python/mypy/issues/1422, should be not hard to fix, but this requires an addition to ``implicit_module_attrs`` in ``nodes.py`` depending on Python version (IIUC they are not present in all versions).
@ilevkivskyi You're right, `__spec__` didn't show up until Python 3.4.
I just bumped on this.  Could we add support for `__spec__`?

ref) https://github.com/lablup/backend.ai/pull/1064

Patch:
diff --git a/mypy/nodes.py b/mypy/nodes.py
--- a/mypy/nodes.py
+++ b/mypy/nodes.py
@@ -117,6 +117,7 @@ def set_line(
     "__file__": "__builtins__.str",
     "__package__": "__builtins__.str",
     "__annotations__": None,  # dict[str, Any] bounded in add_implicit_module_attrs()
+    "__spec__": None,  # importlib.machinery.ModuleSpec bounded in add_implicit_module_attrs()
 }
 
 
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -283,6 +283,7 @@
     TypeVarTupleType,
     TypeVarType,
     UnboundType,
+    UnionType,
     UnpackType,
     get_proper_type,
     get_proper_types,
@@ -635,6 +636,7 @@ def add_implicit_module_attrs(self, file_node: MypyFile) -> None:
         str_type: Type | None = self.named_type_or_none("builtins.str")
         if str_type is None:
             str_type = UnboundType("builtins.str")
+        inst: Type | None
         for name, t in implicit_module_attrs.items():
             if name == "__doc__":
                 typ: Type = str_type
@@ -660,6 +662,22 @@ def add_implicit_module_attrs(self, file_node: MypyFile) -> None:
                     self.defer()
                     return
                 typ = inst
+            elif name == "__spec__":
+                if self.options.use_builtins_fixtures:
+                    inst = self.named_type_or_none("builtins.object")
+                else:
+                    inst = self.named_type_or_none("importlib.machinery.ModuleSpec")
+                if inst is None:
+                    if self.final_iteration:
+                        inst = self.named_type_or_none("builtins.object")
+                        assert inst is not None, "Cannot find builtins.object"
+                    else:
+                        self.defer()
+                        return
+                if file_node.name == "__main__":
+                    # https://docs.python.org/3/reference/import.html#main-spec
+                    inst = UnionType.make_union([inst, NoneType()])
+                typ = inst
             else:
                 assert t is not None, f"type should be specified for {name}"
                 typ = UnboundType(t)


Test patch:
diff --git a/test-data/unit/check-basic.test b/test-data/unit/check-basic.test
--- a/test-data/unit/check-basic.test
+++ b/test-data/unit/check-basic.test
@@ -219,6 +219,14 @@ reveal_type(__doc__)          # N: Revealed type is "builtins.str"
 reveal_type(__file__)         # N: Revealed type is "builtins.str"
 reveal_type(__package__)      # N: Revealed type is "builtins.str"
 reveal_type(__annotations__)  # N: Revealed type is "builtins.dict[builtins.str, Any]"
+# This will actually reveal Union[importlib.machinery.ModuleSpec, None]
+reveal_type(__spec__)         # N: Revealed type is "Union[builtins.object, None]"
+
+import module
+reveal_type(module.__name__)  # N: Revealed type is "builtins.str"
+# This will actually reveal importlib.machinery.ModuleSpec
+reveal_type(module.__spec__)  # N: Revealed type is "builtins.object"
+[file module.py]
 [builtins fixtures/primitives.pyi]
 
 
diff --git a/test-data/unit/fine-grained-inspect.test b/test-data/unit/fine-grained-inspect.test
--- a/test-data/unit/fine-grained-inspect.test
+++ b/test-data/unit/fine-grained-inspect.test
@@ -236,7 +236,7 @@ class C: ...
 [builtins fixtures/module.pyi]
 [out]
 ==
-{"<pack.bar>": ["C", "__annotations__", "__doc__", "__file__", "__name__", "__package__", "bar", "x"], "ModuleType": ["__file__", "__getattr__"]}
+{"<pack.bar>": ["C", "__annotations__", "__doc__", "__file__", "__name__", "__package__", "__spec__", "bar", "x"], "ModuleType": ["__file__", "__getattr__"]}
 
 [case testInspectModuleDef]
 # inspect2: --show=definition --include-kind tmp/foo.py:2:1
diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test
--- a/test-data/unit/pythoneval.test
+++ b/test-data/unit/pythoneval.test
@@ -107,14 +107,20 @@ f
 [case testModuleAttributes]
 import math
 import typing
+print(type(__spec__))
 print(math.__name__)
+print(math.__spec__.name)
 print(type(math.__dict__))
 print(type(math.__doc__ or ''))
+print(type(math.__spec__).__name__)
 print(math.__class__)
 [out]
+<class 'NoneType'>
+math
 math
 <class 'dict'>
 <class 'str'>
+ModuleSpec
 <class 'module'>
 
 [case testSpecialAttributes]