[ty] Improve error messages for unresolved attribute diagnostics (#20963)

## Summary

- Type checkers (and type-checker authors) think in terms of types, but
I think most Python users think in terms of values. Rather than saying
that a _type_ `X` "has no attribute `foo`" (which I think sounds strange
to many users), say that "an object of type `X` has no attribute `foo`"
- Special-case certain types so that the diagnostic messages read more
like normal English: rather than saying "Type `<class 'Foo'>` has no
attribute `bar`" or "Object of type `<class 'Foo'>` has no attribute
`bar`", just say "Class `Foo` has no attribute `bar`"

## Test Plan

Mdtests and snapshots updated
This commit is contained in:
Alex Waygood 2025-10-19 10:58:25 +01:00 committed by GitHub
parent b6b96d75eb
commit 1f8297cfe6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 102 additions and 63 deletions

View file

@ -366,7 +366,7 @@ on function-like callables:
```py
def f_wrong(c: Callable[[], None]):
# error: [unresolved-attribute] "Type `() -> None` has no attribute `__qualname__`"
# error: [unresolved-attribute] "Object of type `() -> None` has no attribute `__qualname__`"
c.__qualname__
# error: [unresolved-attribute] "Unresolved attribute `__qualname__` on type `() -> None`."

View file

@ -1260,13 +1260,13 @@ def _(flag1: bool, flag2: bool):
C = C1 if flag1 else C2 if flag2 else C3
# error: [possibly-missing-attribute] "Attribute `x` on type `<class 'C1'> | <class 'C2'> | <class 'C3'>` may be missing"
# error: [possibly-missing-attribute] "Attribute `x` may be missing on object of type `<class 'C1'> | <class 'C2'> | <class 'C3'>`"
reveal_type(C.x) # revealed: Unknown | Literal[1, 3]
# error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `<class 'C1'> | <class 'C2'> | <class 'C3'>`"
C.x = 100
# error: [possibly-missing-attribute] "Attribute `x` on type `C1 | C2 | C3` may be missing"
# error: [possibly-missing-attribute] "Attribute `x` may be missing on object of type `C1 | C2 | C3`"
reveal_type(C().x) # revealed: Unknown | Literal[1, 3]
# error: [invalid-assignment] "Object of type `Literal[100]` is not assignable to attribute `x` on type `C1 | C2 | C3`"
@ -1292,7 +1292,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
C = C1 if flag1 else C2 if flag2 else C3
# error: [possibly-missing-attribute] "Attribute `x` on type `<class 'C1'> | <class 'C2'> | <class 'C3'>` may be missing"
# error: [possibly-missing-attribute] "Attribute `x` may be missing on object of type `<class 'C1'> | <class 'C2'> | <class 'C3'>`"
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
# error: [possibly-missing-attribute]
@ -1300,7 +1300,7 @@ def _(flag: bool, flag1: bool, flag2: bool):
# Note: we might want to consider ignoring possibly-missing diagnostics for instance attributes eventually,
# see the "Possibly unbound/undeclared instance attribute" section below.
# error: [possibly-missing-attribute] "Attribute `x` on type `C1 | C2 | C3` may be missing"
# error: [possibly-missing-attribute] "Attribute `x` may be missing on object of type `C1 | C2 | C3`"
reveal_type(C().x) # revealed: Unknown | Literal[1, 2, 3]
# error: [possibly-missing-attribute]
@ -1433,7 +1433,7 @@ def _(flag: bool):
class C2: ...
C = C1 if flag else C2
# error: [unresolved-attribute] "Type `<class 'C1'> | <class 'C2'>` has no attribute `x`"
# error: [unresolved-attribute] "Object of type `<class 'C1'> | <class 'C2'>` has no attribute `x`"
reveal_type(C.x) # revealed: Unknown
# TODO: This should ideally be a `unresolved-attribute` error. We need better union
@ -1771,7 +1771,7 @@ reveal_type(date.day) # revealed: int
reveal_type(date.month) # revealed: int
reveal_type(date.year) # revealed: int
# error: [unresolved-attribute] "Type `Date` has no attribute `century`"
# error: [unresolved-attribute] "Object of type `Date` has no attribute `century`"
reveal_type(date.century) # revealed: Unknown
```

View file

@ -311,7 +311,7 @@ reveal_type(C.f(1)) # revealed: str
The method `f` can not be accessed from an instance of the class:
```py
# error: [unresolved-attribute] "Type `C` has no attribute `f`"
# error: [unresolved-attribute] "Object of type `C` has no attribute `f`"
C().f
```

View file

@ -308,7 +308,7 @@ class B(A): ...
reveal_type(super(B)) # revealed: super
# error: [unresolved-attribute] "Type `super` has no attribute `a`"
# error: [unresolved-attribute] "Object of type `super` has no attribute `a`"
super(B).a
```
@ -436,7 +436,7 @@ def f(x: C | D):
s = super(A, x)
reveal_type(s) # revealed: <super: <class 'A'>, C> | <super: <class 'A'>, D>
# error: [possibly-missing-attribute] "Attribute `b` on type `<super: <class 'A'>, C> | <super: <class 'A'>, D>` may be missing"
# error: [possibly-missing-attribute] "Attribute `b` may be missing on object of type `<super: <class 'A'>, C> | <super: <class 'A'>, D>`"
s.b
def f(flag: bool):
@ -476,7 +476,7 @@ def f(flag: bool):
reveal_type(s.x) # revealed: Unknown | Literal[1, 2]
reveal_type(s.y) # revealed: int | str
# error: [possibly-missing-attribute] "Attribute `a` on type `<super: <class 'B'>, B> | <super: <class 'D'>, D>` may be missing"
# error: [possibly-missing-attribute] "Attribute `a` may be missing on object of type `<super: <class 'B'>, B> | <super: <class 'D'>, D>`"
reveal_type(s.a) # revealed: str
```
@ -619,7 +619,7 @@ class B(A):
# TODO: Once `Self` is supported, this should raise `unresolved-attribute` error
super().a
# error: [unresolved-attribute] "Type `<super: <class 'B'>, B>` has no attribute `a`"
# error: [unresolved-attribute] "Object of type `<super: <class 'B'>, B>` has no attribute `a`"
super(B, B(42)).a
```

View file

@ -758,16 +758,16 @@ def _(flag: bool):
non_data: NonDataDescriptor = NonDataDescriptor()
data: DataDescriptor = DataDescriptor()
# error: [possibly-missing-attribute] "Attribute `non_data` on type `<class 'PossiblyUnbound'>` may be missing"
# error: [possibly-missing-attribute] "Attribute `non_data` may be missing on class `PossiblyUnbound`"
reveal_type(PossiblyUnbound.non_data) # revealed: int
# error: [possibly-missing-attribute] "Attribute `non_data` on type `PossiblyUnbound` may be missing"
# error: [possibly-missing-attribute] "Attribute `non_data` may be missing on object of type `PossiblyUnbound`"
reveal_type(PossiblyUnbound().non_data) # revealed: int
# error: [possibly-missing-attribute] "Attribute `data` on type `<class 'PossiblyUnbound'>` may be missing"
# error: [possibly-missing-attribute] "Attribute `data` may be missing on class `PossiblyUnbound`"
reveal_type(PossiblyUnbound.data) # revealed: int
# error: [possibly-missing-attribute] "Attribute `data` on type `PossiblyUnbound` may be missing"
# error: [possibly-missing-attribute] "Attribute `data` may be missing on object of type `PossiblyUnbound`"
reveal_type(PossiblyUnbound().data) # revealed: int
```

View file

@ -26,9 +26,9 @@ def _(flag: bool):
reveal_type(A.union_declared) # revealed: int | str
# error: [possibly-missing-attribute] "Attribute `possibly_unbound` on type `<class 'A'>` may be missing"
# error: [possibly-missing-attribute] "Attribute `possibly_unbound` may be missing on class `A`"
reveal_type(A.possibly_unbound) # revealed: str
# error: [unresolved-attribute] "Type `<class 'A'>` has no attribute `non_existent`"
# error: [unresolved-attribute] "Class `A` has no attribute `non_existent`"
reveal_type(A.non_existent) # revealed: Unknown
```

View file

@ -247,7 +247,7 @@ X: int = 42
from . import foo
import package
# error: [unresolved-attribute] "Type `<module 'package'>` has no attribute `foo`"
# error: [unresolved-attribute] "Module `package` has no member `foo`"
reveal_type(package.foo.X) # revealed: Unknown
```

View file

@ -95,6 +95,6 @@ def f(x: object):
reveal_type(x.__str__) # revealed: bound method object.__str__() -> str
reveal_type(x.__dict__) # revealed: dict[str, Any]
# error: [unresolved-attribute] "Type `<Protocol with members '__qualname__'>` has no attribute `foo`"
# error: [unresolved-attribute] "Object of type `<Protocol with members '__qualname__'>` has no attribute `foo`"
reveal_type(x.foo) # revealed: Unknown
```

View file

@ -16,7 +16,7 @@ class C:
if flag:
x = 2
# error: [possibly-missing-attribute] "Attribute `x` on type `<class 'C'>` may be missing"
# error: [possibly-missing-attribute] "Attribute `x` may be missing on class `C`"
reveal_type(C.x) # revealed: Unknown | Literal[2]
reveal_type(C.y) # revealed: Unknown | Literal[1]
```

View file

@ -26,7 +26,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/attribute_as
# Diagnostics
```
warning[possibly-missing-attribute]: Attribute `attr` on type `<class 'C'>` may be missing
warning[possibly-missing-attribute]: Attribute `attr` may be missing on class `C`
--> src/mdtest_snippet.py:6:5
|
4 | attr: int = 0
@ -41,7 +41,7 @@ info: rule `possibly-missing-attribute` is enabled by default
```
```
warning[possibly-missing-attribute]: Attribute `attr` on type `C` may be missing
warning[possibly-missing-attribute]: Attribute `attr` may be missing on object of type `C`
--> src/mdtest_snippet.py:9:5
|
8 | instance = C()

View file

@ -23,7 +23,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/attributes.md
# Diagnostics
```
error[unresolved-attribute]: Type `<module 'datetime'>` has no attribute `UTC`
error[unresolved-attribute]: Module `datetime` has no member `UTC`
--> src/main.py:4:13
|
3 | # error: [unresolved-attribute]
@ -38,7 +38,7 @@ info: rule `unresolved-attribute` is enabled by default
```
```
error[unresolved-attribute]: Type `<module 'datetime'>` has no attribute `fakenotreal`
error[unresolved-attribute]: Module `datetime` has no member `fakenotreal`
--> src/main.py:6:13
|
4 | reveal_type(datetime.UTC) # revealed: Unknown

View file

@ -118,7 +118,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md
# Diagnostics
```
error[unresolved-attribute]: Type `<super: <class 'C'>, C>` has no attribute `c`
error[unresolved-attribute]: Object of type `<super: <class 'C'>, C>` has no attribute `c`
--> src/mdtest_snippet.py:19:1
|
17 | super(C, C()).a
@ -133,7 +133,7 @@ info: rule `unresolved-attribute` is enabled by default
```
```
error[unresolved-attribute]: Type `<super: <class 'B'>, C>` has no attribute `b`
error[unresolved-attribute]: Object of type `<super: <class 'B'>, C>` has no attribute `b`
--> src/mdtest_snippet.py:22:1
|
21 | super(B, C()).a
@ -146,7 +146,7 @@ info: rule `unresolved-attribute` is enabled by default
```
```
error[unresolved-attribute]: Type `<super: <class 'B'>, C>` has no attribute `c`
error[unresolved-attribute]: Object of type `<super: <class 'B'>, C>` has no attribute `c`
--> src/mdtest_snippet.py:23:1
|
21 | super(B, C()).a
@ -161,7 +161,7 @@ info: rule `unresolved-attribute` is enabled by default
```
```
error[unresolved-attribute]: Type `<super: <class 'A'>, C>` has no attribute `a`
error[unresolved-attribute]: Object of type `<super: <class 'A'>, C>` has no attribute `a`
--> src/mdtest_snippet.py:25:1
|
23 | super(B, C()).c # error: [unresolved-attribute]
@ -176,7 +176,7 @@ info: rule `unresolved-attribute` is enabled by default
```
```
error[unresolved-attribute]: Type `<super: <class 'A'>, C>` has no attribute `b`
error[unresolved-attribute]: Object of type `<super: <class 'A'>, C>` has no attribute `b`
--> src/mdtest_snippet.py:26:1
|
25 | super(A, C()).a # error: [unresolved-attribute]
@ -189,7 +189,7 @@ info: rule `unresolved-attribute` is enabled by default
```
```
error[unresolved-attribute]: Type `<super: <class 'A'>, C>` has no attribute `c`
error[unresolved-attribute]: Object of type `<super: <class 'A'>, C>` has no attribute `c`
--> src/mdtest_snippet.py:27:1
|
25 | super(A, C()).a # error: [unresolved-attribute]

View file

@ -618,7 +618,7 @@ import importlib
from module2 import importlib as other_importlib
from ty_extensions import TypeOf, static_assert, is_equivalent_to
# error: [unresolved-attribute] "Type `<module 'importlib'>` has no attribute `abc`"
# error: [unresolved-attribute] "Module `importlib` has no member `abc`"
reveal_type(importlib.abc) # revealed: Unknown
reveal_type(other_importlib.abc) # revealed: <module 'importlib.abc'>

View file

@ -672,18 +672,18 @@ Also, the "attributes" on the class definition can not be accessed. Neither on t
on inhabitants of the type defined by the class:
```py
# error: [unresolved-attribute] "Type `<class 'Person'>` has no attribute `name`"
# error: [unresolved-attribute] "Class `Person` has no attribute `name`"
Person.name
def _(P: type[Person]):
# error: [unresolved-attribute] "Type `type[Person]` has no attribute `name`"
# error: [unresolved-attribute] "Object of type `type[Person]` has no attribute `name`"
P.name
def _(p: Person) -> None:
# error: [unresolved-attribute] "Type `Person` has no attribute `name`"
# error: [unresolved-attribute] "Object of type `Person` has no attribute `name`"
p.name
type(p).name # error: [unresolved-attribute] "Type `<class 'dict[str, object]'>` has no attribute `name`"
type(p).name # error: [unresolved-attribute] "Class `dict[str, object]` has no attribute `name`"
```
## Special properties