Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -1685,6 +1685,125 @@ date.year = 2025
date.tz = "UTC"
```

### Setting attributes on unions

Setting attributes on unions where all elements of the union have the attribute is acceptable

```py
from typing import Union

class A:
x: int

class B:
x: int

C = Union[A, B]

a: C = A()
a.x = 42
```

Setting attributes on unions where any element of the union does not have the attribute reports
possibly unbound

```py
from typing import Union, Sequence

class A:
pass

class B:
x: int

C = Union[A, B]

def _(a: C):
a.x = 42 # TODO: error: [possibly-unbound-attribute]
```

The same goes for

```py
from dataclasses import dataclass
from typing import Union, Sequence
from abc import ABC

class Base(ABC):
x: Sequence[bytes] = ()

class Derived(Base):
pass

class Other:
pass

D = Union[Derived, Other]

d: D = Other()

# TODO: error: [possibly-unbound-attribute]
# error: [unresolved-attribute]
d.x = None
```

When setting an attribute on a generic type where the upper bound is a union, after narrowing, an
attribute only present on the narrowed type should be able to be set:

```py
from typing import Union, TypeVar

class A:
pass

class B:
def __init__(self) -> None:
self.x: int = 0

C = TypeVar("C", bound=Union[A, B])

def _(b: C):
if not isinstance(b, B):
raise TypeError("Expected instance of B")
reveal_type(b) # revealed: B
b.x = 42
```

Setting attributes on a generic where the upper bound is a union, and not all elements of the union
have the attribute, also reports possibly unbound:

```py
from typing import Union, TypeVar

class A:
pass

class B:
x: int

C = TypeVar("C", bound=Union[A, B])
Copy link
Contributor Author

@thejchap thejchap Jun 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should address the beartype regression this doesn't catch the issue/fail on the other branch

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sharkdp

making slow progress on these - limited time :)

i'm still unclear on why the warning in beartype (warning[possibly-unbound-attribute] beartype/_util/api/external/utilclick.py:108:5: Attribute callbackon typeBeartypeableT is possibly unbound) is no longer being emitted on #18347

however while investigating i did find what i think is an issue on main with setting attributes on generics where the upper bound is a union, after narrowing. added a test to illustrate

this results in a possibly-unbound-attribute from ty but no diagnostic from mypy

mypy

../test.py:16: note: Revealed type is "test.B"
Success: no issues found in 1 source file

ty

info[revealed-type]: Revealed type
  --> /Users/justinchapman/src/test.py:16:23
   |
14 |     if not isinstance(b, B):
15 |         raise TypeError("Expected instance of B")
16 |     print(reveal_type(b))
   |                       ^ `C & B`
17 |     b.x = 42
   |

code

from typing import Union, TypeVar
from typing_extensions import reveal_type

class A:
    pass

class B:
    def __init__(self) -> None:
        self.x: int = 0

C = TypeVar("C", bound=Union[A, B])

def _(b: C):
    if not isinstance(b, B):
        raise TypeError("Expected instance of B")
    print(reveal_type(b))
    b.x = 42

b = B()
_(b)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That looks like a bug, yes. A similar bug was mentioned on Discord the other day. Would you mind reporting that as a separate issue? A simplified example would be https://play.ty.dev/62d91bdb-cbf1-4758-8f2c-6a26722c9f47

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


def _(a: C):
a.x = 42 # error: [possibly-unbound-attribute]
```

### Assigning to a data descriptor attribute
Copy link
Contributor Author

@thejchap thejchap Jun 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should address the datadog regression doesn't seem to


This is invalid

```py
class A:
def __init__(self, x: int):
self.x = x

@property
def y(self) -> int:
return self.x

a = A(42)
a.y = 1 # error: [invalid-assignment] "Invalid assignment to data descriptor attribute `y` on type `A` with custom `__set__` method"
```

### `argparse.Namespace`

A standard library example of a class with a custom `__setattr__` method is `argparse.Namespace`:
Expand Down