Skip to content

Commit 150ea92

Browse files
authored
[ty] Add tests for instance attributes in class hierarchies (#20767)
## Summary This adds a couple of new test cases related to astral-sh/ty#1067 and beyond that. For now, they are just documenting the current (problematic) behavior. Since the topic has some subtleties, I'd like to merge this prior to the actual bugfix(es) in order to evaluate the changes in an easier way.
1 parent 697998f commit 150ea92

File tree

1 file changed

+69
-13
lines changed

1 file changed

+69
-13
lines changed

crates/ty_python_semantic/resources/mdtest/attributes.md

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -820,22 +820,30 @@ reveal_type(C().c) # revealed: int
820820

821821
### Inheritance of class/instance attributes
822822

823-
#### Instance variable defined in a base class
824-
825823
```py
826824
class Base:
827-
declared_in_body: int | None = 1
825+
attribute: int | None = 1
826+
827+
redeclared_with_same_type: str | None
828+
redeclared_with_narrower_type: str | None
829+
redeclared_with_wider_type: str | None
830+
831+
overwritten_in_subclass_body: str
832+
overwritten_in_subclass_method: str
828833

829-
base_class_attribute_1: str | None
830-
base_class_attribute_2: str | None
831-
base_class_attribute_3: str | None
834+
undeclared = "base"
832835

833836
def __init__(self) -> None:
834-
self.defined_in_init: str | None = "value in base"
837+
self.pure_attribute: str | None = "value in base"
838+
839+
self.pure_overwritten_in_subclass_body: str = "value in base"
840+
self.pure_overwritten_in_subclass_method: str = "value in base"
841+
842+
self.pure_undeclared = "base"
835843

836844
class Intermediate(Base):
837845
# Redeclaring base class attributes with the *same *type is fine:
838-
base_class_attribute_1: str | None = None
846+
redeclared_with_same_type: str | None = None
839847

840848
# Redeclaring them with a *narrower type* is unsound, because modifications
841849
# through a `Base` reference could violate that constraint.
@@ -847,22 +855,70 @@ class Intermediate(Base):
847855
# enabled by default can still be discussed.
848856
#
849857
# TODO: This should be an error
850-
base_class_attribute_2: str
858+
redeclared_with_narrower_type: str
851859

852860
# Redeclaring attributes with a *wider type* directly violates LSP.
853861
#
854862
# In this case, both mypy and pyright report an error.
855863
#
856864
# TODO: This should be an error
857-
base_class_attribute_3: str | int | None
865+
redeclared_with_wider_type: str | int | None
866+
867+
# TODO: This should be an `invalid-assignment` error
868+
overwritten_in_subclass_body = None
869+
870+
# TODO: This should be an `invalid-assignment` error
871+
pure_overwritten_in_subclass_body = None
872+
873+
undeclared = "intermediate"
874+
875+
def set_attributes(self) -> None:
876+
# TODO: This should be an `invalid-assignment` error
877+
self.overwritten_in_subclass_method = None
878+
879+
# TODO: This should be an `invalid-assignment` error
880+
self.pure_overwritten_in_subclass_method = None
881+
882+
self.pure_undeclared = "intermediate"
858883

859884
class Derived(Intermediate): ...
860885

861-
reveal_type(Derived.declared_in_body) # revealed: int | None
886+
reveal_type(Derived.attribute) # revealed: int | None
887+
reveal_type(Derived().attribute) # revealed: int | None
888+
889+
reveal_type(Derived.redeclared_with_same_type) # revealed: str | None
890+
reveal_type(Derived().redeclared_with_same_type) # revealed: str | None
891+
892+
# TODO: It would probably be more consistent if these were `str | None`
893+
reveal_type(Derived.redeclared_with_narrower_type) # revealed: str
894+
reveal_type(Derived().redeclared_with_narrower_type) # revealed: str
895+
896+
# TODO: It would probably be more consistent if these were `str | None`
897+
reveal_type(Derived.redeclared_with_wider_type) # revealed: str | int | None
898+
reveal_type(Derived().redeclared_with_wider_type) # revealed: str | int | None
899+
900+
# TODO: Both of these should be `str`
901+
reveal_type(Derived.overwritten_in_subclass_body) # revealed: Unknown | None
902+
reveal_type(Derived().overwritten_in_subclass_body) # revealed: Unknown | None | str
903+
904+
# TODO: Both of these should be `str`
905+
reveal_type(Derived.overwritten_in_subclass_method) # revealed: str
906+
reveal_type(Derived().overwritten_in_subclass_method) # revealed: str | Unknown | None
907+
908+
reveal_type(Derived().pure_attribute) # revealed: str | None
909+
910+
# TODO: This should be `str`
911+
reveal_type(Derived().pure_overwritten_in_subclass_body) # revealed: Unknown | None | str
912+
913+
# TODO: This should be `str`
914+
reveal_type(Derived().pure_overwritten_in_subclass_method) # revealed: Unknown | None
862915

863-
reveal_type(Derived().declared_in_body) # revealed: int | None
916+
# TODO: Both of these should be `Unknown | Literal["intermediate", "base"]`
917+
reveal_type(Derived.undeclared) # revealed: Unknown | Literal["intermediate"]
918+
reveal_type(Derived().undeclared) # revealed: Unknown | Literal["intermediate"]
864919

865-
reveal_type(Derived().defined_in_init) # revealed: str | None
920+
# TODO: This should be `Unknown | Literal["intermediate", "base"]`
921+
reveal_type(Derived().pure_undeclared) # revealed: Unknown | Literal["intermediate"]
866922
```
867923

868924
## Accessing attributes on class objects

0 commit comments

Comments
 (0)