Skip to content

Commit

Permalink
[PEP 695] Allow covariance with attribute that has "_" name prefix (#…
Browse files Browse the repository at this point in the history
…17782)

Fix this conformance test:
```
class ShouldBeCovariant5[T]:
    def __init__(self, x: T) -> None:
        self._x = x

    @Property
    def x(self) -> T:
        return self._x

vo5_1: ShouldBeCovariant5[float] = ShouldBeCovariant5[int](1)  # OK
vo5_2: ShouldBeCovariant5[int] = ShouldBeCovariant5[float](1)  # E
```

My fix is to treat such attributes as not settable when inferring
variance.

Link:

https://github.com/python/typing/blob/main/conformance/tests/generics_variance_inference.py#L79
  • Loading branch information
JukkaL committed Sep 18, 2024
1 parent a47f301 commit fce14a0
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 6 deletions.
22 changes: 16 additions & 6 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2006,16 +2006,22 @@ def infer_variance(info: TypeInfo, i: int) -> bool:
for member in all_non_object_members(info):
if member in ("__init__", "__new__"):
continue
node = info[member].node
if isinstance(node, Var) and node.type is None:
tv.variance = VARIANCE_NOT_READY
return False

if isinstance(self_type, TupleType):
self_type = mypy.typeops.tuple_fallback(self_type)

flags = get_member_flags(member, self_type)
typ = find_member(member, self_type, self_type)
settable = IS_SETTABLE in flags

node = info[member].node
if isinstance(node, Var):
if node.type is None:
tv.variance = VARIANCE_NOT_READY
return False
if has_underscore_prefix(member):
# Special case to avoid false positives (and to pass conformance tests)
settable = False

typ = find_member(member, self_type, self_type)
if typ:
typ2 = expand_type(typ, {tvar.id: object_type})
if not is_subtype(typ, typ2):
Expand All @@ -2036,6 +2042,10 @@ def infer_variance(info: TypeInfo, i: int) -> bool:
return True


def has_underscore_prefix(name: str) -> bool:
return name.startswith("_") and not (name.startswith("__") and name.endswith("__"))


def infer_class_variances(info: TypeInfo) -> bool:
if not info.defn.type_args:
return True
Expand Down
63 changes: 63 additions & 0 deletions test-data/unit/check-python312.test
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,69 @@ class Invariant[T]:

reveal_type(c(a1, a2)) # N: Revealed type is "Never"

[case testPEP695InferVarianceUnderscorePrefix]
# flags: --enable-incomplete-feature=NewGenericSyntax

class Covariant1[T]:
def __init__(self, x: T) -> None:
self._x = x

@property
def x(self) -> T:
return self._x

co1_1: Covariant1[float] = Covariant1[int](1)
co1_2: Covariant1[int] = Covariant1[float](1) # E: Incompatible types in assignment (expression has type "Covariant1[float]", variable has type "Covariant1[int]")

class Covariant2[T]:
def __init__(self, x: T) -> None:
self.__foo_bar = x

@property
def x(self) -> T:
return self.__foo_bar

co2_1: Covariant2[float] = Covariant2[int](1)
co2_2: Covariant2[int] = Covariant2[float](1) # E: Incompatible types in assignment (expression has type "Covariant2[float]", variable has type "Covariant2[int]")

class Invariant1[T]:
def __init__(self, x: T) -> None:
self._x = x

# Methods behave differently from attributes
def _f(self, x: T) -> None: ...

@property
def x(self) -> T:
return self._x

inv1_1: Invariant1[float] = Invariant1[int](1) # E: Incompatible types in assignment (expression has type "Invariant1[int]", variable has type "Invariant1[float]")
inv1_2: Invariant1[int] = Invariant1[float](1) # E: Incompatible types in assignment (expression has type "Invariant1[float]", variable has type "Invariant1[int]")

class Invariant2[T]:
def __init__(self, x: T) -> None:
# Dunders are special
self.__x__ = x

@property
def x(self) -> T:
return self.__x__

inv2_1: Invariant2[float] = Invariant2[int](1) # E: Incompatible types in assignment (expression has type "Invariant2[int]", variable has type "Invariant2[float]")
inv2_2: Invariant2[int] = Invariant2[float](1) # E: Incompatible types in assignment (expression has type "Invariant2[float]", variable has type "Invariant2[int]")

class Invariant3[T]:
def __init__(self, x: T) -> None:
self._x = Invariant1(x)

@property
def x(self) -> T:
return self._x._x

inv3_1: Invariant3[float] = Invariant3[int](1) # E: Incompatible types in assignment (expression has type "Invariant3[int]", variable has type "Invariant3[float]")
inv3_2: Invariant3[int] = Invariant3[float](1) # E: Incompatible types in assignment (expression has type "Invariant3[float]", variable has type "Invariant3[int]")
[builtins fixtures/property.pyi]

[case testPEP695InheritInvariant]
# flags: --enable-incomplete-feature=NewGenericSyntax

Expand Down

0 comments on commit fce14a0

Please sign in to comment.