Skip to content

Commit ce040d2

Browse files
committed
[ty] Add attribute assignment tests for unions
1 parent 3eada01 commit ce040d2

File tree

1 file changed

+71
-5
lines changed

1 file changed

+71
-5
lines changed

crates/ty_python_semantic/resources/mdtest/attributes.md

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ reveal_type(c_instance.declared_and_bound) # revealed: bool
7474

7575
#### Variable declared in class body and possibly bound in `__init__`
7676

77-
The same rule applies even if the variable is *declared* (not bound!) in the class body: it is still
77+
The same rule applies even if the variable is _declared_ (not bound!) in the class body: it is still
7878
a pure instance variable.
7979

8080
```py
@@ -780,7 +780,7 @@ reveal_type(c_instance.variable_with_class_default1) # revealed: str
780780
#### Descriptor attributes as class variables
781781

782782
Whether they are explicitly qualified as `ClassVar`, or just have a class level default, we treat
783-
descriptor attributes as class variables. This test mainly makes sure that we do *not* treat them as
783+
descriptor attributes as class variables. This test mainly makes sure that we do _not_ treat them as
784784
instance variables. This would lead to a different outcome, since the `__get__` method would not be
785785
called (the descriptor protocol is not invoked for instance variables).
786786

@@ -897,7 +897,7 @@ def _(flag: bool):
897897
reveal_type(C3.attr2) # revealed: Literal["metaclass value", "class value"]
898898
```
899899

900-
If the *metaclass* attribute is only partially defined, we emit a `possibly-unbound-attribute`
900+
If the _metaclass_ attribute is only partially defined, we emit a `possibly-unbound-attribute`
901901
diagnostic:
902902

903903
```py
@@ -1400,7 +1400,7 @@ def _(a_and_b: Intersection[A, B]):
14001400

14011401
The union of the set of types that `Any` could materialise to is equivalent to `object`. It follows
14021402
from this that attribute access on `Any` resolves to `Any` if the attribute does not exist on
1403-
`object` -- but if the attribute *does* exist on `object`, the type of the attribute is
1403+
`object` -- but if the attribute _does_ exist on `object`, the type of the attribute is
14041404
`<type as it exists on object> & Any`.
14051405

14061406
```py
@@ -1628,6 +1628,70 @@ date.year = 2025
16281628
date.tz = "UTC"
16291629
```
16301630

1631+
### Setting attributes on unions
1632+
1633+
Setting attributes on unions where all elements of the union have the attribute is acceptable
1634+
1635+
```py
1636+
from typing import Union
1637+
1638+
class A:
1639+
x: int
1640+
1641+
class B:
1642+
x: int
1643+
1644+
C = Union[A, B]
1645+
1646+
a: C = A()
1647+
a.x = 42
1648+
```
1649+
1650+
Setting attributes on unions where any element of the union does not have the attribute reports
1651+
possibly unbound
1652+
1653+
```py
1654+
from typing import Union
1655+
1656+
class A:
1657+
pass
1658+
1659+
class B:
1660+
x: int
1661+
1662+
C = Union[A, B]
1663+
1664+
a: C = A()
1665+
1666+
# instead of unresolved-attribute, this should report possibly-unbound-attribute
1667+
# TODO: error: [possibly-unbound-attribute]
1668+
# error: [unresolved-attribute]
1669+
a.x = 42
1670+
1671+
a: C = B()
1672+
1673+
# TODO: error: [possibly-unbound-attribute]
1674+
a.x = 42
1675+
```
1676+
1677+
Setting attributes on a generic where the upper bound is a union, and not all elements of the union
1678+
have the attribute, also reports possibly unbound:
1679+
1680+
```py
1681+
from typing import Union, TypeVar
1682+
1683+
class A:
1684+
pass
1685+
1686+
class B:
1687+
x: int
1688+
1689+
C = TypeVar("C", bound=Union[A, B])
1690+
1691+
def _(a: C):
1692+
a.x = 42 # error: [possibly-unbound-attribute]
1693+
```
1694+
16311695
### `argparse.Namespace`
16321696

16331697
A standard library example of a class with a custom `__setattr__` method is `argparse.Namespace`:
@@ -1732,11 +1796,13 @@ for mod.global_symbol in IntIterable():
17321796
`outer/__init__.py`:
17331797

17341798
```py
1799+
17351800
```
17361801

17371802
`outer/nested/__init__.py`:
17381803

17391804
```py
1805+
17401806
```
17411807

17421808
`outer/nested/inner.py`:
@@ -2111,7 +2177,7 @@ reveal_type(Foo.__members__) # revealed: @Todo(Attribute access on enum classes
21112177

21122178
## References
21132179

2114-
Some of the tests in the *Class and instance variables* section draw inspiration from
2180+
Some of the tests in the _Class and instance variables_ section draw inspiration from
21152181
[pyright's documentation] on this topic.
21162182

21172183
[descriptor protocol tests]: descriptor_protocol.md

0 commit comments

Comments
 (0)