Repository: python/mypy

Issue:
Mypy crash when @staticmethod has the same name as an attr
Summary of the issue:

When using `attrs` to create a dataclass, adding a static method to the class with the same name as one of the attributes causes `mypy` to crash.

I am happy to work on a fix here, but would need some guidance on where to start.

Please provide more information to help us understand the issue:

* Are you reporting a bug, or opening a feature request?

This is a bug - `mypy` should not crash when running against files.

* Please insert below the code you are checking with mypy,
  or a mock-up repro if the source is private. We would appreciate
  if you try to simplify your case to a minimal repro.

The following is an minimal reproduction of the issue:
```python
import attr

@attr.s
class MyData:
    is_foo: bool = attr.ib()

    @staticmethod
    def is_foo(string: str) -> bool:
        return False
```

* What is the actual behavior/output?

When run against the above file, `mypy` crashes with the following error output:

```bash
$~ mypy test.py --show-traceback
test.py:5: error: INTERNAL ERROR -- please report a bug at https://github.com/python/mypy/issues version: 0.701
Traceback (most recent call last):
  File "/home/james/.local/share/virtualenvs/web-findings-dozQMylR/bin/mypy", line 10, in <module>
    sys.exit(console_entry())
  File "mypy/semanal.py", line 3791, in accept
  File "mypy/nodes.py", line 895, in accept__Node_glue
  File "mypy/nodes.py", line 896, in accept
  File "mypy/semanal.py", line 804, in visit_class_def__StatementVisitor_glue
  File "mypy/semanal.py", line 807, in visit_class_def
  File "mypy/semanal.py", line 825, in analyze_class
  File "mypy/semanal.py", line 833, in analyze_class_body_common
  File "mypy/semanal.py", line 895, in apply_class_plugin_hooks
  File "mypy/plugins/attrs.py", line 211, in attr_class_maker_callback
  File "mypy/plugins/attrs.py", line 267, in _analyze_class
AssertionError:
test.py:5: : note: use --pdb to drop into pdb
```

* What is the behavior/output you expect?

I'm not 100% what I would expect here. I think that `mypy` should probably raise an error about the redefinition of something with the same name, as that is what happens if I change the `staticmethod` to a regular classmethod.

* What are the versions of mypy and Python you are using?

Python 3.6.8
mypy 0.701

* Do you see the same issue after installing mypy from Git master?

Yes, although the traceback is a bit more detailed:

```bash
test.py:4: error: INTERNAL ERROR -- please report a bug at https://github.com/python/mypy/issues version: 0.710+dev.f0c2506eddf2a06e1d830953541b5d431e50073e
test.py:4: : note: use --pdb to drop into pdb
Traceback (most recent call last):
  File "/path/to/my/virtualenv/bin/mypy", line 10, in <module>
    sys.exit(console_entry())
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/__main__.py", line 7, in console_entry
    main(None)
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/main.py", line 85, in main
    res = build.build(sources, options, None, flush_errors, fscache)
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/build.py", line 163, in build
    result = _build(sources, options, alt_lib_path, flush_errors, fscache)
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/build.py", line 218, in _build
    graph = dispatch(sources, manager)
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/build.py", line 2461, in dispatch
    process_graph(graph, manager)
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/build.py", line 2761, in process_graph
    process_stale_scc(graph, scc, manager)
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/build.py", line 2862, in process_stale_scc
    graph[id].semantic_analysis()
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/build.py", line 1997, in semantic_analysis
    self.manager.semantic_analyzer.visit_file(self.tree, self.xpath, self.options, patches)
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/semanal.py", line 309, in visit_file
    self.accept(d)
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/semanal.py", line 3786, in accept
    node.accept(self)
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/nodes.py", line 901, in accept
    return visitor.visit_class_def(self)
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/semanal.py", line 807, in visit_class_def
    self.analyze_class(defn)
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/semanal.py", line 825, in analyze_class
    self.analyze_class_body_common(defn)
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/semanal.py", line 833, in analyze_class_body_common
    self.apply_class_plugin_hooks(defn)
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/semanal.py", line 895, in apply_class_plugin_hooks
    hook(ClassDefContext(defn, decorator, self))
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/plugins/attrs.py", line 211, in attr_class_maker_callback
    attributes = _analyze_class(ctx, auto_attribs, kw_only)
  File "/path/to/my/virtualenv/lib/python3.6/site-packages/mypy/plugins/attrs.py", line 267, in _analyze_class
    assert isinstance(node, Var)
AssertionError: 
```

* What are the mypy flags you are using? (For example --strict-optional)

The local `setup.cfg` file has the following section:

```ini
[mypy]
ignore_missing_imports = True
check_untyped_defs = True
```


Discussion:
Looks like a bug in the mypy plugin for attrs, as indicated by the failing `assert`. I can't help you further except to suggest that you read the code, and run it with `print()` calls added (or trace it using a debugger like `pdb`) to figure out how it works.
Worth noting that the reason the stack trace is nicer on master than in the release is that the release is using a mypyc-compiled binary.
This is the same as https://github.com/python/mypy/issues/6493 but with `dataclasses` -> `attrs`, the two plugins are very similar, so the existing PR that fixes the other issue can be easily updated to fix also this one, see https://github.com/python/mypy/pull/6644.
Note https://github.com/python/mypy/issues/7918 has an example when this crash appears in another situation (also involving decorators).
I can no longer reproduce the error OP experienced using latest top of tree.

It now says `error: Name "is_foo" already defined on line 5`, which is correct.
We're still seeing this issue (or rather https://github.com/python/mypy/issues/7918 / https://github.com/python/mypy/issues/6493) with 0.991:

```python
from attrs import frozen

@frozen(kw_only=True)
class Base:
    name: str

@frozen(kw_only=True)
class Sub(Base):
    first_name: str
    last_name: str
    
    @property
    def name(self) -> str:
        return f"{self.first_name} {self.last_name}"

```

Even if this wasn't allowed, it would be great if mypy gave an error message instead of crashing with no indication where and/or why:

```
  main()
  File "mypy/main.py", line 95, in main
  File "mypy/main.py", line 174, in run_build
  File "mypy/build.py", line 193, in build
  File "mypy/build.py", line 276, in _build
  File "mypy/build.py", line 2903, in dispatch
  File "mypy/build.py", line 3291, in process_graph
  File "mypy/build.py", line 3386, in process_stale_scc
  File "mypy/semanal_main.py", line 90, in semantic_analysis_for_scc
  File "mypy/semanal_main.py", line 429, in apply_class_plugin_hooks
  File "mypy/semanal_main.py", line 457, in apply_hooks_to_class
  File "mypy/plugins/attrs.py", line 332, in attr_class_maker_callback
  File "mypy/plugins/attrs.py", line 739, in _make_frozen
AssertionError
```
Yes, I confirm this crashes on current master.

Resolving patch:
diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py
--- a/mypy/plugins/attrs.py
+++ b/mypy/plugins/attrs.py
@@ -736,7 +736,11 @@ def _make_frozen(ctx: mypy.plugin.ClassDefContext, attributes: list[Attribute])
         if attribute.name in ctx.cls.info.names:
             # This variable belongs to this class so we can modify it.
             node = ctx.cls.info.names[attribute.name].node
-            assert isinstance(node, Var)
+            if not isinstance(node, Var):
+                # The superclass attribute was overridden with a non-variable.
+                # No need to do anything here, override will be verified during
+                # type checking.
+                continue
             node.is_property = True
         else:
             # This variable belongs to a super class so create new Var so we


Test patch:
diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test
--- a/test-data/unit/check-attr.test
+++ b/test-data/unit/check-attr.test
@@ -1788,3 +1788,49 @@ class C:
 c = C(x=[C.D()])
 reveal_type(c.x)  # N: Revealed type is "builtins.list[__main__.C.D]"
 [builtins fixtures/list.pyi]
+
+[case testRedefinitionInFrozenClassNoCrash]
+import attr
+
+@attr.s
+class MyData:
+    is_foo: bool = attr.ib()
+
+    @staticmethod  # E: Name "is_foo" already defined on line 5
+    def is_foo(string: str) -> bool: ...
+[builtins fixtures/classmethod.pyi]
+
+[case testOverrideWithPropertyInFrozenClassNoCrash]
+from attrs import frozen
+
+@frozen(kw_only=True)
+class Base:
+    name: str
+
+@frozen(kw_only=True)
+class Sub(Base):
+    first_name: str
+    last_name: str
+
+    @property
+    def name(self) -> str: ...
+[builtins fixtures/property.pyi]
+
+[case testOverrideWithPropertyInFrozenClassChecked]
+from attrs import frozen
+
+@frozen(kw_only=True)
+class Base:
+    name: str
+
+@frozen(kw_only=True)
+class Sub(Base):
+    first_name: str
+    last_name: str
+
+    @property
+    def name(self) -> int: ...  # E: Signature of "name" incompatible with supertype "Base"
+
+# This matches runtime semantics
+reveal_type(Sub)  # N: Revealed type is "def (*, name: builtins.str, first_name: builtins.str, last_name: builtins.str) -> __main__.Sub"
+[builtins fixtures/property.pyi]