Skip to content

Commit 738692b

Browse files
thejchapsharkdp
andauthored
[ty] Fix __setattr__ call check precedence during attribute assignment (#18347)
## Summary Related: - astral-sh/ty#111 - #17974 (comment) Previously, when validating an attribute assignment, a `__setattr__` call check was only done if the attribute wasn't found as either a class member or instance member This PR changes the `__setattr__` call check to be attempted first, prior to the "[normal mechanism](https://docs.python.org/3/reference/datamodel.html#object.__setattr__)", as a defined `__setattr__` should take precedence over setting an attribute on the instance dictionary directly. if the return type of `__setattr__` is `Never`, an `invalid-assignment` diagnostic is emitted Once this is merged, a subsequent PR will synthesize a `__setattr__` method with a `Never` return type for frozen dataclasses. ## Test Plan Existing tests + mypy_primer --------- Co-authored-by: David Peter <mail@david-peter.de>
1 parent 9a4b85d commit 738692b

2 files changed

Lines changed: 231 additions & 131 deletions

File tree

crates/ty_python_semantic/resources/mdtest/attributes.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1791,6 +1791,80 @@ date.year = 2025
17911791
date.tz = "UTC"
17921792
```
17931793

1794+
### Return type of `__setattr__`
1795+
1796+
If the return type of the `__setattr__` method is `Never`, we do not allow any attribute assignments
1797+
on instances of that class:
1798+
1799+
```py
1800+
from typing_extensions import Never
1801+
1802+
class Frozen:
1803+
existing: int = 1
1804+
1805+
def __setattr__(self, name, value) -> Never:
1806+
raise AttributeError("Attributes can not be modified")
1807+
1808+
instance = Frozen()
1809+
instance.non_existing = 2 # error: [invalid-assignment] "Cannot assign to attribute `non_existing` on type `Frozen` whose `__setattr__` method returns `Never`/`NoReturn`"
1810+
instance.existing = 2 # error: [invalid-assignment] "Cannot assign to attribute `existing` on type `Frozen` whose `__setattr__` method returns `Never`/`NoReturn`"
1811+
```
1812+
1813+
### `__setattr__` on `object`
1814+
1815+
`object` has a custom `__setattr__` implementation, but we still emit an error if a non-existing
1816+
attribute is assigned on an `object` instance.
1817+
1818+
```py
1819+
obj = object()
1820+
obj.non_existing = 1 # error: [unresolved-attribute]
1821+
```
1822+
1823+
### Setting attributes on `Never` / `Any`
1824+
1825+
Setting attributes on `Never` itself should be allowed (even though it has a `__setattr__` attribute
1826+
of type `Never`):
1827+
1828+
```py
1829+
from typing_extensions import Never, Any
1830+
1831+
def _(n: Never):
1832+
reveal_type(n.__setattr__) # revealed: Never
1833+
1834+
# No error:
1835+
n.non_existing = 1
1836+
```
1837+
1838+
And similarly for `Any`:
1839+
1840+
```py
1841+
def _(a: Any):
1842+
reveal_type(a.__setattr__) # revealed: Any
1843+
1844+
# No error:
1845+
a.non_existing = 1
1846+
```
1847+
1848+
### Possibly unbound `__setattr__` method
1849+
1850+
If a `__setattr__` method is only partially bound, the behavior is still the same:
1851+
1852+
```py
1853+
from typing_extensions import Never
1854+
1855+
def flag() -> bool:
1856+
return True
1857+
1858+
class Frozen:
1859+
if flag():
1860+
def __setattr__(self, name, value) -> Never:
1861+
raise AttributeError("Attributes can not be modified")
1862+
1863+
instance = Frozen()
1864+
instance.non_existing = 2 # error: [invalid-assignment]
1865+
instance.existing = 2 # error: [invalid-assignment]
1866+
```
1867+
17941868
### `argparse.Namespace`
17951869

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

0 commit comments

Comments
 (0)