Repository: python/mypy

Issue:
mypyc-related stubtest crash
**Bug Report**

In typeshed CI stubtest crashes when run against fpdf2 2.7.5 and the current typeshed stubs. It works fine with fpdf2 2.7.4. Please see python/typeshed#10533, or to be more specific the traceback at https://github.com/python/typeshed/actions/runs/5930009431/job/16078844153?pr=10533:

```
Traceback (most recent call last):
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/opt/hostedtoolcache/Python/3.10.12/x64/lib/python3.10/runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "/tmp/tmpo2a_8672/lib/python3.10/site-packages/mypy/stubtest.py", line 1891, in <module>
    sys.exit(main())
  File "/tmp/tmpo2a_8672/lib/python3.10/site-packages/mypy/stubtest.py", line 1887, in main
    return test_stubs(parse_options(sys.argv[1:]))
  File "/tmp/tmpo2a_8672/lib/python3.10/site-packages/mypy/stubtest.py", line 1760, in test_stubs
    for error in test_module(module):
  File "/tmp/tmpo2a_8672/lib/python3.10/site-packages/mypy/stubtest.py", line 223, in test_module
    yield from verify(stub, runtime, [module_name])
  File "/tmp/tmpo2a_8672/lib/python3.10/site-packages/mypy/stubtest.py", line 401, in verify_mypyfile
    yield from verify(stub_entry, runtime_entry, object_path + [entry])
  File "/tmp/tmpo2a_8672/lib/python3.10/site-packages/mypy/stubtest.py", line 543, in verify_typeinfo
    yield from verify(stub_to_verify, runtime_attr, object_path + [entry])
  File "/tmp/tmpo2a_8672/lib/python3.10/site-packages/mypy/stubtest.py", line [100](https://github.com/python/typeshed/actions/runs/5930009431/job/16078844153?pr=10533#step:5:101)6, in verify_funcitem
    for message in _verify_signature(stub_sig, runtime_sig, function_name=stub.name):
  File "/tmp/tmpo2a_8672/lib/python3.10/site-packages/mypy/stubtest.py", line 858, in _verify_signature
    yield from _verify_arg_default_value(stub_arg, runtime_arg)
  File "/tmp/tmpo2a_8672/lib/python3.10/site-packages/mypy/stubtest.py", line 632, in _verify_arg_default_value
    runtime_type = get_mypy_type_of_runtime_value(runtime_arg.default)
  File "/tmp/tmpo2a_8672/lib/python3.10/site-packages/mypy/stubtest.py", line 1563, in get_mypy_type_of_runtime_value
    return mypy.types.LiteralType(value=value, fallback=fallback)
  File "mypy/types.py", line 2584, in __init__
TypeError: union[int, str, bool, float] object expected; got None
```

This error seems to be generated by mypyc. I can't reproduce it locally, possibly because mypyc is not used?

**Your Environment**

- Mypy version used: 1.5.1
- Mypy command-line flags:
```
/tmp/tmpo2a_8672/bin/pip install fpdf2[]==2.7.5 mypy==1.5.1
MYPYPATH=/home/runner/work/typeshed/typeshed/stubs/fpdf2:/home/runner/work/typeshed/typeshed/stubs/Pillow /tmp/tmpo2a_8672/bin/python -m mypy.stubtest --custom-typeshed-dir /home/runner/work/typeshed/typeshed fpdf --allowlist /home/runner/work/typeshed/typeshed/stubs/fpdf2/@tests/stubtest_allowlist.txt
```
- Python version used: 3.10.12



Discussion:
I can reproduce this crash locally on Python 3.9 and Python 3.10, but not Python 3.11
> I can reproduce this crash locally on Python 3.9 and Python 3.10, but not Python 3.11

And, as @srittau says, the crash only occurs with a compiled version of mypy, which makes it tricky to debug!
Looks like the TypeError is raised when mypy is trying to validate the default value typeshed gives for one of the parameters in this `__init__` method: https://github.com/py-pdf/fpdf2/blob/ff55d983a0b6d601a78628a45747234fbca59e26/fpdf/encryption.py#L127-L135
I confirmed locally that applying this diff to typeshed's stubs makes the crash go away:

```diff
--- a/stubs/fpdf2/fpdf/encryption.pyi
+++ b/stubs/fpdf2/fpdf/encryption.pyi
@@ -65,15 +65,6 @@ class StandardSecurityHandler:
     k: str
     u: str

-    def __init__(
-        self,
-        fpdf: FPDF,
-        owner_password: str,
-        user_password: str | None = None,
-        permission: Incomplete | None = None,
-        encryption_method: EncryptionMethod | None = None,
-        encrypt_metadata: bool = False,
-    ) -> None: ...
     def generate_passwords(self, file_id) -> None: ...
     def get_encryption_obj(self) -> EncryptionDictionary: ...
     def encrypt(self, text: str | bytes | bytearray, obj_id) -> bytes: ...
```
The issue is this call here:

https://github.com/python/mypy/blob/7141d6bcff9e26e774e88712015ca6bbe8307c9e/mypy/stubtest.py#L1563

When attempting to validate the `permission` parameter on `fpdf.encryption.StandardSecurityHandler.__init__`, stubtest ends up trying to call `mypy.types.LiteralType(value=None, fallback=fpdf.enums.AccessPermission)`. That goes against the type annotation for `mypy.types.LiteralType.__init__`, which says that `value` should only ever be `int`, `str`, `bool` or `float`:

https://github.com/python/mypy/blob/7141d6bcff9e26e774e88712015ca6bbe8307c9e/mypy/types.py#L2560-L2578

Inserting a `reveal_type()` call indicates that _mypy_ believes that `value` can only be an `int` or a `str` here, which is why the type violation is only being caught by the runtime checks that mypyc inserts when compiling mypy:

```diff
--- a/mypy/stubtest.py
+++ b/mypy/stubtest.py
@@ -1560,6 +1560,7 @@ def get_mypy_type_of_runtime_value(runtime: Any) -> mypy.types.Type | None:
     else:
         return fallback

+    reveal_type(value)
     return mypy.types.LiteralType(value=value, fallback=fallback)
```

```
> python runtests.py self
run self: ['C:\\Users\\alexw\\coding\\mypy\\venv\\Scripts\\python.exe', '-m', 'mypy', '--config-file', 'mypy_self_check.ini', '-p', 'mypy', '-p', 'mypyc']
mypy\stubtest.py:1563: note: Revealed type is "Union[builtins.int, builtins.str]"
```
Here is the reason why stubtest crashes on Python <=3.10 but not on Python 3.11. On Python 3.10:

```pycon
Python 3.10.12 | packaged by Anaconda, Inc. | (main, Jul  5 2023, 19:01:18) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import fpdf.encryption
>>> fpdf.encryption.AccessPermission.all().name is None
True
```

On Python 3.11:

```pycon
Python 3.11.4 | packaged by Anaconda, Inc. | (main, Jul  5 2023, 13:47:18) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import fpdf.encryption
>>> fpdf.encryption.AccessPermission.all().name
'PRINT_LOW_RES|MODIFY|COPY|ANNOTATION|FILL_FORMS|COPY_FOR_ACCESSIBILITY|ASSEMBLE|PRINT_HIGH_RES'
```

`isinstance(fpdf.encryption.AccessPermission.all(), enum.Enum)` evaluates to `True`, which means that `value` is set to `fpdf.encryption.AccessPermission.all().name` here:

https://github.com/python/mypy/blob/7141d6bcff9e26e774e88712015ca6bbe8307c9e/mypy/stubtest.py#L1556-L1557

But on Python <=3.10, `fpdf.encryption.AccessPermission.all().name` is `None`, which triggers the crash.

The `fpdf.encryption.AccessPermissions` `IntFlag` is defined here: https://github.com/py-pdf/fpdf2/blob/34ff166de50a757c8cb32d6f047f0901e052d814/fpdf/enums.py#L801-L834
The reason why mypy doesn't pick up that `runtime.name` could be `None` here during the self-check is because `enum.Flag` violates the Liskov Substitution Principle. The `name` property can only ever be a `str` on an instance of an `enum.Enum` enumeration... unless that enumeration is a subclass of `IntFlag`, in which case it could also be `None`: https://github.com/python/typeshed/blob/32b750b6aa2396cdaa757da16da8cd8fb627c09f/stdlib/enum.pyi#L216-L217

Resolving patch:
diff --git a/mypy/stubtest.py b/mypy/stubtest.py
--- a/mypy/stubtest.py
+++ b/mypy/stubtest.py
@@ -1553,7 +1553,7 @@ def anytype() -> mypy.types.AnyType:
     value: bool | int | str
     if isinstance(runtime, bytes):
         value = bytes_to_human_readable_repr(runtime)
-    elif isinstance(runtime, enum.Enum):
+    elif isinstance(runtime, enum.Enum) and isinstance(runtime.name, str):
         value = runtime.name
     elif isinstance(runtime, (bool, int, str)):
         value = runtime


Test patch:
diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py
--- a/mypy/test/teststubtest.py
+++ b/mypy/test/teststubtest.py
@@ -64,6 +64,7 @@ def __init__(self, name: str) -> None: ...
 
 class Coroutine(Generic[_T_co, _S, _R]): ...
 class Iterable(Generic[_T_co]): ...
+class Iterator(Iterable[_T_co]): ...
 class Mapping(Generic[_K, _V]): ...
 class Match(Generic[AnyStr]): ...
 class Sequence(Iterable[_T_co]): ...
@@ -86,7 +87,9 @@ def __init__(self) -> None: pass
     def __repr__(self) -> str: pass
 class type: ...
 
-class tuple(Sequence[T_co], Generic[T_co]): ...
+class tuple(Sequence[T_co], Generic[T_co]):
+    def __ge__(self, __other: tuple[T_co, ...]) -> bool: pass
+
 class dict(Mapping[KT, VT]): ...
 
 class function: pass
@@ -105,6 +108,39 @@ def classmethod(f: T) -> T: ...
 def staticmethod(f: T) -> T: ...
 """
 
+stubtest_enum_stub = """
+import sys
+from typing import Any, TypeVar, Iterator
+
+_T = TypeVar('_T')
+
+class EnumMeta(type):
+    def __len__(self) -> int: pass
+    def __iter__(self: type[_T]) -> Iterator[_T]: pass
+    def __reversed__(self: type[_T]) -> Iterator[_T]: pass
+    def __getitem__(self: type[_T], name: str) -> _T: pass
+
+class Enum(metaclass=EnumMeta):
+    def __new__(cls: type[_T], value: object) -> _T: pass
+    def __repr__(self) -> str: pass
+    def __str__(self) -> str: pass
+    def __format__(self, format_spec: str) -> str: pass
+    def __hash__(self) -> Any: pass
+    def __reduce_ex__(self, proto: Any) -> Any: pass
+    name: str
+    value: Any
+
+class Flag(Enum):
+    def __or__(self: _T, other: _T) -> _T: pass
+    def __and__(self: _T, other: _T) -> _T: pass
+    def __xor__(self: _T, other: _T) -> _T: pass
+    def __invert__(self: _T) -> _T: pass
+    if sys.version_info >= (3, 11):
+        __ror__ = __or__
+        __rand__ = __and__
+        __rxor__ = __xor__
+"""
+
 
 def run_stubtest(
     stub: str, runtime: str, options: list[str], config_file: str | None = None
@@ -114,6 +150,8 @@ def run_stubtest(
             f.write(stubtest_builtins_stub)
         with open("typing.pyi", "w") as f:
             f.write(stubtest_typing_stub)
+        with open("enum.pyi", "w") as f:
+            f.write(stubtest_enum_stub)
         with open(f"{TEST_MODULE_NAME}.pyi", "w") as f:
             f.write(stub)
         with open(f"{TEST_MODULE_NAME}.py", "w") as f:
@@ -954,16 +992,15 @@ def fizz(self): pass
 
     @collect_cases
     def test_enum(self) -> Iterator[Case]:
+        yield Case(stub="import enum", runtime="import enum", error=None)
         yield Case(
             stub="""
-            import enum
             class X(enum.Enum):
                 a: int
                 b: str
                 c: str
             """,
             runtime="""
-            import enum
             class X(enum.Enum):
                 a = 1
                 b = "asdf"
@@ -971,6 +1008,66 @@ class X(enum.Enum):
             """,
             error="X.c",
         )
+        yield Case(
+            stub="""
+            class Flags1(enum.Flag):
+                a: int
+                b: int
+            def foo(x: Flags1 = ...) -> None: ...
+            """,
+            runtime="""
+            class Flags1(enum.Flag):
+                a = 1
+                b = 2
+            def foo(x=Flags1.a|Flags1.b): pass
+            """,
+            error=None,
+        )
+        yield Case(
+            stub="""
+            class Flags2(enum.Flag):
+                a: int
+                b: int
+            def bar(x: Flags2 | None = None) -> None: ...
+            """,
+            runtime="""
+            class Flags2(enum.Flag):
+                a = 1
+                b = 2
+            def bar(x=Flags2.a|Flags2.b): pass
+            """,
+            error="bar",
+        )
+        yield Case(
+            stub="""
+            class Flags3(enum.Flag):
+                a: int
+                b: int
+            def baz(x: Flags3 | None = ...) -> None: ...
+            """,
+            runtime="""
+            class Flags3(enum.Flag):
+                a = 1
+                b = 2
+            def baz(x=Flags3(0)): pass
+            """,
+            error=None,
+        )
+        yield Case(
+            stub="""
+            class Flags4(enum.Flag):
+                a: int
+                b: int
+            def spam(x: Flags4 | None = None) -> None: ...
+            """,
+            runtime="""
+            class Flags4(enum.Flag):
+                a = 1
+                b = 2
+            def spam(x=Flags4(0)): pass
+            """,
+            error="spam",
+        )
 
     @collect_cases
     def test_decorator(self) -> Iterator[Case]: