Repository: python/mypy

Issue:
AssertionError: Internal error: too many class plugin hook passes
**Crash Report**

See the traceback.

**Traceback**

```
Traceback (most recent call last):
  File "D:\dro\bin\WPy64-31090\python-3.10.9.amd64\lib\runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "D:\dro\bin\WPy64-31090\python-3.10.9.amd64\lib\runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "D:\dro\bin\WPy64-31090\python-3.10.9.amd64\scripts\mypy.exe\__main__.py", line 7, in <module>
  File "D:\dro\bin\WPy64-31090\python-3.10.9.amd64\lib\site-packages\mypy\__main__.py", line 15, in console_entry
    main()
  File "mypy\main.py", line 95, in main
  File "mypy\main.py", line 174, in run_build
  File "mypy\build.py", line 194, in build
  File "mypy\build.py", line 277, in _build
  File "mypy\build.py", line 2923, in dispatch
  File "mypy\build.py", line 3320, in process_graph
  File "mypy\build.py", line 3415, in process_stale_scc
  File "mypy\semanal_main.py", line 99, in semantic_analysis_for_scc
  File "mypy\semanal_main.py", line 429, in apply_class_plugin_hooks
AssertionError: Internal error: too many class plugin hook passes
```

**To Reproduce**

```python
#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-
###############################################################################
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from decimal import Context, Decimal, Inexact
from typing import ClassVar, Protocol


DecimalPlus = Decimal | str | int | float


@dataclass
class TargetSettings(Protocol):
    max: DecimalPlus
    min: DecimalPlus
    inc: DecimalPlus
    precision: int

    PLACES: Decimal = field(init=False, repr=False)
    INEXACT: ClassVar[Context] = Context(traps=[Inexact])

    def __post_init__(self) -> None:
        self.PLACES = PLACES = Decimal(10) ** -self.precision

        self.min = Decimal(self.min).quantize(PLACES)
        self.max = Decimal(self.max).quantize(PLACES)
        self.inc = Decimal(self.inc).quantize(PLACES)

    def is_valid(self, val: DecimalPlus) -> bool:
        try:
            val = Decimal(val).quantize(self.PLACES, context=self.INEXACT)
        except Inexact:
            return False  # more decimal places than allowed

        # mypy does not know attributes are Decimals at this stage
        if not (self.min <= val <= self.max):  # type: ignore
            return False

        return True  # all checks passed

```

**Your Environment**

- Mypy version used: 1.1.1
- Mypy command-line flags: `None`
- Mypy configuration options from `mypy.ini` (and other config files): `None`
- Python version used: `3.10.9`
- Operating system and version: `Windows 11 22621.1413`

**Additional Comment**

Let me say that I do believe that the concept of static type checking of `Union` is broken (again: *the concept*). In my very humble opinion as user, if I do declare something as a `Union` (either with the keyword or the `|` operator), `mypy` must believe that when the time comes, the variable/attribute declared as a Union, will contain one of the types.

Reporting errors like 
```
error: Unsupported operand types for <= ("Decimal" and "str")  [operator]
```

when the `Union` contains `Union[Decimal, str, int, float]` is in my very humble opinion: **wrong**. Such an error can be reported during *runtime*, but not during static analysis, because `mypy` has no way of knowing what the type at that stage contains. Reporting a "warning" would be ok. Reporting an "error" is an error itself.

No, as a user I am not in the mood of adding `cast` here and there and here-there to pollute the code, hence the need to add a `# type: ignore` to silence `mypy`.


Discussion:
Here's a smaller reproducer:

```python
from dataclasses import dataclass
from typing import Protocol

@dataclass
class TargetSettings(Protocol):
    min: int

    def __post_init__(self) -> None:
        self.min = int(self.min)
```

Combining `@dataclass` and `Protocol` is a strange thing to do and I'm not sure it should be allowed. However, mypy should not crash.
I do understand that `@dataclass` may make no sense for a `Protocol`, but this is, imho, bound to happen again, assuming typing becomes the mainstream development path.

How it happened in this case:

  - A `dataclass` ended up being used by a secondary class class and `mypy` was complaining that the referenced `dataclass` didn't have attribute X. Because it was defined later in the code.
  - The `dataclass` vital parts were taken out and put into a `Protocol` defined before the secondary class which means the secondary class can reference the protocol and be happy
  - Because the final class is a `dataclass` it seemed sensible to also have the `Protocol` class being decorated as a `dataclass`


Resolving patch:
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -3647,7 +3647,7 @@ def analyze_lvalue(
                 has_explicit_value=has_explicit_value,
             )
         elif isinstance(lval, MemberExpr):
-            self.analyze_member_lvalue(lval, explicit_type, is_final)
+            self.analyze_member_lvalue(lval, explicit_type, is_final, has_explicit_value)
             if explicit_type and not self.is_self_member_ref(lval):
                 self.fail("Type cannot be declared in assignment to non-self attribute", lval)
         elif isinstance(lval, IndexExpr):
@@ -3824,7 +3824,9 @@ def analyze_tuple_or_list_lvalue(self, lval: TupleExpr, explicit_type: bool = Fa
                     has_explicit_value=True,
                 )
 
-    def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool, is_final: bool) -> None:
+    def analyze_member_lvalue(
+        self, lval: MemberExpr, explicit_type: bool, is_final: bool, has_explicit_value: bool
+    ) -> None:
         """Analyze lvalue that is a member expression.
 
         Arguments:
@@ -3853,12 +3855,18 @@ def analyze_member_lvalue(self, lval: MemberExpr, explicit_type: bool, is_final:
                 and explicit_type
             ):
                 self.attribute_already_defined(lval.name, lval, cur_node)
-            # If the attribute of self is not defined in superclasses, create a new Var, ...
+            if self.type.is_protocol and has_explicit_value and cur_node is not None:
+                # Make this variable non-abstract, it would be safer to do this only if we
+                # are inside __init__, but we do this always to preserve historical behaviour.
+                if isinstance(cur_node.node, Var):
+                    cur_node.node.is_abstract_var = False
             if (
+                # If the attribute of self is not defined, create a new Var, ...
                 node is None
-                or (isinstance(node.node, Var) and node.node.is_abstract_var)
+                # ... or if it is defined as abstract in a *superclass*.
+                or (cur_node is None and isinstance(node.node, Var) and node.node.is_abstract_var)
                 # ... also an explicit declaration on self also creates a new Var.
-                # Note that `explicit_type` might has been erased for bare `Final`,
+                # Note that `explicit_type` might have been erased for bare `Final`,
                 # so we also check if `is_final` is passed.
                 or (cur_node is None and (explicit_type or is_final))
             ):


Test patch:
diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test
--- a/test-data/unit/check-dataclasses.test
+++ b/test-data/unit/check-dataclasses.test
@@ -2037,3 +2037,16 @@ Foo(
     present_5=5,
 )
 [builtins fixtures/dataclasses.pyi]
+
+[case testProtocolNoCrash]
+from typing import Protocol, Union, ClassVar
+from dataclasses import dataclass, field
+
+DEFAULT = 0
+
+@dataclass
+class Test(Protocol):
+    x: int
+    def reset(self) -> None:
+        self.x = DEFAULT
+[builtins fixtures/dataclasses.pyi]
diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test
--- a/test-data/unit/check-protocols.test
+++ b/test-data/unit/check-protocols.test
@@ -4054,3 +4054,14 @@ class P(Protocol):
 
 [file lib.py]
 class C: ...
+
+[case testAllowDefaultConstructorInProtocols]
+from typing import Protocol
+
+class P(Protocol):
+    x: int
+    def __init__(self, x: int) -> None:
+        self.x = x
+
+class C(P): ...
+C(0)  # OK