diff --git a/docs/changelog.md b/docs/changelog.md index 19f723e2..eba3ef7f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ ## Unreleased +- Change implementation of implicit int/float and float/complex promotion + in accordance with https://github.com/python/typing/pull/1748. Now, + annotations of `float` implicitly mean `float | int`. +- Fix assignability for certain combinations of unions, `Annotated`, and `NewType`. - Reduce more uninhabited intersections to `Never` ## Version 0.2.0 (June 26, 2025) diff --git a/pycroscope/annotations.py b/pycroscope/annotations.py index 224cf702..0822374e 100644 --- a/pycroscope/annotations.py +++ b/pycroscope/annotations.py @@ -1342,6 +1342,10 @@ def _maybe_typed_value(val: Union[type, str]) -> Value: return HashableProtoValue elif val is Callable or is_typing_name(val, "Callable"): return CallableValue(ANY_SIGNATURE) + elif val is float: + return TypedValue(float) | TypedValue(int) + elif val is complex: + return TypedValue(complex) | TypedValue(float) | TypedValue(int) return TypedValue(val) diff --git a/pycroscope/arg_spec.py b/pycroscope/arg_spec.py index e91615d4..e474f9e9 100644 --- a/pycroscope/arg_spec.py +++ b/pycroscope/arg_spec.py @@ -1062,10 +1062,7 @@ def _get_generic_bases_cached(self, typ: Union[type, str]) -> GenericBases: assert isinstance( typ, type ), f"failed to extract typeshed bases for {typ!r}" - bases = [ - type_from_runtime(base, ctx=self.default_context) - for base in self.get_runtime_bases(typ) - ] + bases = [self._type_from_base(base) for base in self.get_runtime_bases(typ)] generic_bases = self._extract_bases(typ, bases) assert ( generic_bases is not None @@ -1073,6 +1070,14 @@ def _get_generic_bases_cached(self, typ: Union[type, str]) -> GenericBases: self.generic_bases_cache[typ] = generic_bases return generic_bases + def _type_from_base(self, base: object) -> Value: + # Avoid promoting float to float|int here. + if base is float: + return TypedValue(float) + elif base is complex: + return TypedValue(complex) + return type_from_runtime(base, ctx=self.default_context) + def _extract_bases( self, typ: Union[type, str], bases: Optional[Sequence[Value]] ) -> Optional[GenericBases]: diff --git a/pycroscope/relations.py b/pycroscope/relations.py index 00ce328d..3e50dc36 100644 --- a/pycroscope/relations.py +++ b/pycroscope/relations.py @@ -191,7 +191,15 @@ def _has_relation( right: GradualType, relation: Literal[Relation.SUBTYPE, Relation.ASSIGNABLE], ctx: CanAssignContext, + *, + original_left: Optional[GradualType] = None, + original_right: Optional[GradualType] = None, ) -> CanAssign: + if original_right is None: + original_right = right + if original_left is None: + original_left = left + # TypeVarValue if isinstance(left, TypeVarValue): if left == right: @@ -224,7 +232,7 @@ def _has_relation( # AnnotatedValue if isinstance(left, AnnotatedValue): left_inner = gradualize(left.value) - can_assign = _has_relation(left_inner, right, relation, ctx) + can_assign = _has_relation(left_inner, right, relation, ctx, original_left=left) if isinstance(can_assign, CanAssignError): return can_assign bounds_maps = [can_assign] @@ -234,9 +242,11 @@ def _has_relation( return custom_can_assign bounds_maps.append(custom_can_assign) return unify_bounds_maps(bounds_maps) - if isinstance(right, AnnotatedValue) and not isinstance(left, MultiValuedValue): + if isinstance(right, AnnotatedValue): right_inner = gradualize(right.value) - can_assign = _has_relation(left, right_inner, relation, ctx) + can_assign = _has_relation( + left, right_inner, relation, ctx, original_right=right + ) if isinstance(can_assign, CanAssignError): return can_assign bounds_maps = [can_assign] @@ -247,19 +257,34 @@ def _has_relation( bounds_maps.append(custom_can_assign) return unify_bounds_maps(bounds_maps) + # NewTypeValue + if isinstance(left, NewTypeValue): + if isinstance(right, NewTypeValue): + if left.newtype is right.newtype: + return {} + else: + return CanAssignError(f"{right} is not {relation.description} {left}") + elif isinstance(right, AnyValue): + pass + else: + return CanAssignError(f"{right} is not {relation.description} {left}") + if isinstance(right, NewTypeValue): + right_inner = gradualize(right.value) + return _has_relation(left, right_inner, relation, ctx, original_right=right) + # IntersectionValue if isinstance(left, IntersectionValue): # Try to simplify first left = intersect_multi(left.vals, ctx) if not isinstance(left, IntersectionValue): - return _has_relation(left, right, relation, ctx) + return _has_relation(left, original_right, relation, ctx) if isinstance(right, IntersectionValue): right = intersect_multi(right.vals, ctx) # Must be a subtype of all the members bounds_maps = [] errors = [] for val in left.vals: - can_assign = _has_relation(gradualize(val), right, relation, ctx) + can_assign = _has_relation(gradualize(val), original_right, relation, ctx) if isinstance(can_assign, CanAssignError): errors.append(can_assign) else: @@ -272,12 +297,12 @@ def _has_relation( if isinstance(right, IntersectionValue): right = intersect_multi(right.vals, ctx) if not isinstance(right, IntersectionValue): - return _has_relation(left, right, relation, ctx) + return _has_relation(original_left, right, relation, ctx) # At least one member must be a subtype bounds_maps = [] errors = [] for val in right.vals: - can_assign = _has_relation(left, gradualize(val), relation, ctx) + can_assign = _has_relation(original_left, gradualize(val), relation, ctx) if isinstance(can_assign, CanAssignError): errors.append(can_assign) else: @@ -304,7 +329,7 @@ def _has_relation( errors = [] for val in left.vals: val = gradualize(val) - can_assign = _has_relation(val, right, relation, ctx) + can_assign = _has_relation(val, original_right, relation, ctx) if isinstance(can_assign, CanAssignError): errors.append(can_assign) else: @@ -326,7 +351,7 @@ def _has_relation( bounds_maps = [] for val in right.vals: val = gradualize(val) - can_assign = _has_relation(left, val, relation, ctx) + can_assign = _has_relation(original_left, val, relation, ctx) if isinstance(can_assign, CanAssignError): # Adding an additional layer here isn't helpful return can_assign @@ -359,6 +384,7 @@ def _has_relation( return {} # Any is assignable to everything else: assert_never(relation) + assert not isinstance(left, NewTypeValue) # SyntheticModuleValue if isinstance(left, SyntheticModuleValue): @@ -395,19 +421,6 @@ def _has_relation( if isinstance(right, ParamSpecKwargsValue): return has_relation(left, right.get_fallback_value(), relation, ctx) - # NewTypeValue - if isinstance(left, NewTypeValue): - if isinstance(right, NewTypeValue): - if left.newtype is right.newtype: - return {} - else: - return CanAssignError(f"{right} is not {relation.description} {left}") - else: - return CanAssignError(f"{right} is not {relation.description} {left}") - if isinstance(right, NewTypeValue): - right_inner = gradualize(right.value) - return _has_relation(left, right_inner, relation, ctx) - # UnboundMethodValue if isinstance(left, UnboundMethodValue): if isinstance(right, UnboundMethodValue) and left == right: diff --git a/pycroscope/test_annotations.py b/pycroscope/test_annotations.py index b49d670a..138250d5 100644 --- a/pycroscope/test_annotations.py +++ b/pycroscope/test_annotations.py @@ -37,7 +37,7 @@ def capybara() -> Union[int, str]: def kerodon() -> Optional[int]: return None - def complex() -> Union[List[str], Set[int], Dict[float, List[str]], int]: + def complex() -> Union[List[str], Set[int], Dict[bytes, List[str]], int]: return [] def union_in_subscript() -> List[Union[str, int]]: @@ -58,7 +58,7 @@ def check() -> None: GenericValue(set, [TypedValue(int)]), GenericValue( dict, - [TypedValue(float), GenericValue(list, [TypedValue(str)])], + [TypedValue(bytes), GenericValue(list, [TypedValue(str)])], ), TypedValue(int), ] @@ -767,14 +767,14 @@ def capybara( y: Callable[[int], str], id_func: Callable[[T], T], takes_seq: Callable[[Sequence[T]], T], - two_args: Callable[[int, str], float], + two_args: Callable[[int, str], bytes], ): assert_is_value(x(), TypedValue(int)) assert_is_value(x(arg=3), TypedValue(int)) assert_is_value(y(1), TypedValue(str)) assert_is_value(id_func(1), KnownValue(1)) assert_is_value(takes_seq([int("1")]), TypedValue(int)) - assert_is_value(two_args(1, "x"), TypedValue(float)) + assert_is_value(two_args(1, "x"), TypedValue(bytes)) @assert_passes() def test_stringified(self): @@ -787,14 +787,14 @@ def capybara( y: "Callable[[int], str]", id_func: "Callable[[T], T]", takes_seq: "Callable[[Sequence[T]], T]", - two_args: "Callable[[int, str], float]", + two_args: "Callable[[int, str], bytes]", ): assert_is_value(x(), TypedValue(int)) assert_is_value(x(arg=3), TypedValue(int)) assert_is_value(y(1), TypedValue(str)) assert_is_value(id_func(1), KnownValue(1)) assert_is_value(takes_seq([int("1")]), TypedValue(int)) - assert_is_value(two_args(1, "x"), TypedValue(float)) + assert_is_value(two_args(1, "x"), TypedValue(bytes)) @assert_passes() def test_abc_callable(self): @@ -808,14 +808,14 @@ def capybara( y: Callable[[int], str], id_func: Callable[[T], T], takes_seq: Callable[[Sequence[T]], T], - two_args: Callable[[int, str], float], + two_args: Callable[[int, str], bytes], ): assert_is_value(x(), TypedValue(int)) assert_is_value(x(arg=3), TypedValue(int)) assert_is_value(y(1), TypedValue(str)) assert_is_value(id_func(1), KnownValue(1)) assert_is_value(takes_seq([int("1")]), TypedValue(int)) - assert_is_value(two_args(1, "x"), TypedValue(float)) + assert_is_value(two_args(1, "x"), TypedValue(bytes)) @assert_passes() def test_known_value(self): @@ -1055,21 +1055,24 @@ def test_callable_compatibility(self): AnyStr = TypeVar("AnyStr", bytes, str) IntT = TypeVar("IntT", bound=int) - class SupportsIsInteger(Protocol): - def is_integer(self) -> bool: + class SupportsInt(Protocol): + def __int__(self) -> int: raise NotImplementedError - SupportsIsIntegerT = TypeVar("SupportsIsIntegerT", bound=SupportsIsInteger) + SupportsIntT = TypeVar("SupportsIntT", bound=SupportsInt) - def find_int(objs: Iterable[SupportsIsIntegerT]) -> SupportsIsIntegerT: + def find_int(objs: Iterable[SupportsIntT]) -> SupportsIntT: for obj in objs: - if obj.is_integer(): + if obj.__int__() == obj: return obj raise ValueError def wants_float_func(f: Callable[[Iterable[float]], float]) -> float: return f([1.0, 2.0]) + def wants_int_func(f: Callable[[Iterable[int]], int]) -> int: + return f([1, 2]) + def want_anystr_func( f: Callable[[AnyStr], AnyStr], s: Union[str, bytes] ) -> str: @@ -1099,7 +1102,10 @@ def capybara(): want_bounded_func(anystr_func, 1) # E: incompatible_argument want_str_func(anystr_func) want_str_func(int_func) # E: incompatible_argument - wants_float_func(find_int) + assert_is_value(find_int([1.0, 2.0]), KnownValue(1.0) | KnownValue(2.0)) + # TODO this should work with wants_float_func but it doesn't. + # Some bug with binding TypeVars to a union? + wants_int_func(find_int) wants_float_func(int_func) # E: incompatible_argument @assert_passes() @@ -1515,7 +1521,7 @@ def test_typing_extensions(self): class RNR(TypedDict): a: int b: Required[str] - c: NotRequired[float] + c: NotRequired[bytes] def take_rnr(td: RNR) -> None: assert_is_value( @@ -1524,7 +1530,7 @@ def take_rnr(td: RNR) -> None: { "a": TypedDictEntry(TypedValue(int)), "b": TypedDictEntry(TypedValue(str)), - "c": TypedDictEntry(TypedValue(float), required=False), + "c": TypedDictEntry(TypedValue(bytes), required=False), } ), ) @@ -1532,7 +1538,7 @@ def take_rnr(td: RNR) -> None: class NotTotal(TypedDict, total=False): a: int b: Required[str] - c: NotRequired[float] + c: NotRequired[bytes] def take_not_total(td: NotTotal) -> None: assert_is_value( @@ -1541,7 +1547,7 @@ def take_not_total(td: NotTotal) -> None: { "a": TypedDictEntry(TypedValue(int), required=False), "b": TypedDictEntry(TypedValue(str)), - "c": TypedDictEntry(TypedValue(float), required=False), + "c": TypedDictEntry(TypedValue(bytes), required=False), } ), ) @@ -1549,7 +1555,7 @@ def take_not_total(td: NotTotal) -> None: class Stringify(TypedDict): a: "int" b: "Required[str]" - c: "NotRequired[float]" + c: "NotRequired[bytes]" def take_stringify(td: Stringify) -> None: assert_is_value( @@ -1558,7 +1564,7 @@ def take_stringify(td: Stringify) -> None: { "a": TypedDictEntry(TypedValue(int)), "b": TypedDictEntry(TypedValue(str)), - "c": TypedDictEntry(TypedValue(float), required=False), + "c": TypedDictEntry(TypedValue(bytes), required=False), } ), ) @@ -1572,7 +1578,7 @@ def test_typeddict_from_call(self): class Stringify(TypedDict): a: "int" b: "Required[str]" - c: "NotRequired[float]" + c: "NotRequired[bytes]" def make_td() -> Any: return Stringify @@ -1591,7 +1597,7 @@ def capybara() -> None: { "a": TypedDictEntry(TypedValue(int)), "b": TypedDictEntry(TypedValue(str)), - "c": TypedDictEntry(TypedValue(float), required=False), + "c": TypedDictEntry(TypedValue(bytes), required=False), } ), ) @@ -1602,7 +1608,7 @@ def capybara() -> None: { "a": TypedDictEntry(TypedValue(int)), "b": TypedDictEntry(TypedValue(str)), - "c": TypedDictEntry(TypedValue(float), required=False), + "c": TypedDictEntry(TypedValue(bytes), required=False), } ), ) @@ -1616,7 +1622,7 @@ def test_typing(self): class RNR(TypedDict): a: int b: Required[str] - c: NotRequired[float] + c: NotRequired[bytes] def take_rnr(td: RNR) -> None: assert_is_value( @@ -1625,7 +1631,7 @@ def take_rnr(td: RNR) -> None: { "a": TypedDictEntry(TypedValue(int)), "b": TypedDictEntry(TypedValue(str)), - "c": TypedDictEntry(TypedValue(float), required=False), + "c": TypedDictEntry(TypedValue(bytes), required=False), } ), ) @@ -1633,7 +1639,7 @@ def take_rnr(td: RNR) -> None: class NotTotal(TypedDict, total=False): a: int b: Required[str] - c: NotRequired[float] + c: NotRequired[bytes] def take_not_total(td: NotTotal) -> None: assert_is_value( @@ -1642,7 +1648,7 @@ def take_not_total(td: NotTotal) -> None: { "a": TypedDictEntry(TypedValue(int), required=False), "b": TypedDictEntry(TypedValue(str)), - "c": TypedDictEntry(TypedValue(float), required=False), + "c": TypedDictEntry(TypedValue(bytes), required=False), } ), ) @@ -1650,7 +1656,7 @@ def take_not_total(td: NotTotal) -> None: class Stringify(TypedDict): a: "int" b: "Required[str]" - c: "NotRequired[float]" + c: "NotRequired[bytes]" def take_stringify(td: Stringify) -> None: assert_is_value( @@ -1659,7 +1665,7 @@ def take_stringify(td: Stringify) -> None: { "a": TypedDictEntry(TypedValue(int)), "b": TypedDictEntry(TypedValue(str)), - "c": TypedDictEntry(TypedValue(float), required=False), + "c": TypedDictEntry(TypedValue(bytes), required=False), } ), ) @@ -2057,6 +2063,80 @@ def capybara(t: "TypedCapybara", u: "UntypedCapybara") -> None: assert_type(u.y, Any) +class TestFloatInt(TestNameCheckVisitorBase): + @assert_passes() + def test(self): + from typing_extensions import assert_type + + def capybara(x: float): + assert_is_value(x, TypedValue(float) | TypedValue(int)) + assert_type(x, float) + + if isinstance(x, float): + # can't express this type for assert_type() + assert_is_value(x, TypedValue(float)) + else: + assert_is_value(x, TypedValue(int)) + assert_type(x, int) + + @assert_passes() + def test_complex(self): + from typing_extensions import assert_type + + def capybara(x: complex): + assert_is_value( + x, TypedValue(complex) | TypedValue(float) | TypedValue(int) + ) + assert_type(x, complex) + + if isinstance(x, float): + # can't express this type for assert_type() + assert_is_value(x, TypedValue(float)) + else: + assert_is_value(x, TypedValue(int) | TypedValue(complex)) + + @assert_passes() + def test_cast(self): + from typing import cast + + from typing_extensions import assert_type + + def capybara(x): + f = cast(float, x) + assert_is_value(f, TypedValue(float) | TypedValue(int)) + assert_type(f, float) + + @assert_passes() + def test_float_subclass(self): + from typing_extensions import assert_type + + class MyFloat(float): + pass + + def capybara(x: MyFloat): + assert_is_value(x, TypedValue(MyFloat)) + assert_type(x, MyFloat) + + def caller(): + capybara(MyFloat(1.0)) + capybara(1.0) # E: incompatible_argument + + @assert_passes() + def test_complex_subclass(self): + from typing_extensions import assert_type + + class MyComplex(complex): + pass + + def capybara(x: MyComplex): + assert_is_value(x, TypedValue(MyComplex)) + assert_type(x, MyComplex) + + def caller(): + capybara(MyComplex(1.0)) + capybara(1.0j) # E: incompatible_argument + + class TestProtocol(TestNameCheckVisitorBase): @assert_passes() def test_conditional_annotations(self): diff --git a/pycroscope/test_attributes.py b/pycroscope/test_attributes.py index 8320e601..ac8a2304 100644 --- a/pycroscope/test_attributes.py +++ b/pycroscope/test_attributes.py @@ -19,7 +19,7 @@ assert_is_value, ) -_global_dict: Dict[Union[int, str], float] = {} +_global_dict: Dict[Union[int, str], bytes] = {} class TestAttributes(TestNameCheckVisitorBase): @@ -112,11 +112,11 @@ class B: x: str class C(B): - y: float + y: bytes def capybara() -> None: assert_is_value(A().x, TypedValue(int)) - assert_is_value(C().y, TypedValue(float)) + assert_is_value(C().y, TypedValue(bytes)) assert_is_value(C().x, TypedValue(str)) @assert_passes() @@ -329,7 +329,7 @@ def capybara(): assert_is_value( test_attributes._global_dict, GenericValue( - dict, [TypedValue(int) | TypedValue(str), TypedValue(float)] + dict, [TypedValue(int) | TypedValue(str), TypedValue(bytes)] ), ) assert_is_value( diff --git a/pycroscope/test_generators.py b/pycroscope/test_generators.py index a5cfbc3d..1c573c17 100644 --- a/pycroscope/test_generators.py +++ b/pycroscope/test_generators.py @@ -10,18 +10,18 @@ class TestGenerator(TestNameCheckVisitorBase): def test_generator_return(self): from typing import Generator - def gen(cond) -> Generator[int, str, float]: + def gen(cond) -> Generator[int, str, bytes]: x = yield 1 assert_is_value(x, TypedValue(str)) yield "x" # E: incompatible_yield if cond: - return 3.0 + return b"hello" else: return "hello" # E: incompatible_return_value def capybara() -> Generator[int, int, int]: x = yield from gen(True) # E: incompatible_yield - assert_is_value(x, TypedValue(float)) + assert_is_value(x, TypedValue(bytes)) return 3 diff --git a/pycroscope/test_implementation.py b/pycroscope/test_implementation.py index 0e2ba78f..b747eb8c 100644 --- a/pycroscope/test_implementation.py +++ b/pycroscope/test_implementation.py @@ -1154,10 +1154,10 @@ def capybara(x): def test_tuple_annotation(self): from typing import Tuple - def capybara(tpl: Tuple[int, str, float]) -> None: + def capybara(tpl: Tuple[int, str, bytes]) -> None: assert_is_value(tpl[0], TypedValue(int)) assert_is_value(tpl[-2], TypedValue(str)) - assert_is_value(tpl[2], TypedValue(float)) + assert_is_value(tpl[2], TypedValue(bytes)) @assert_passes() def test_list_in_lambda(self): @@ -1244,7 +1244,7 @@ def test_complex_incomplete(self): from typing_extensions import NotRequired, TypedDict class TD(TypedDict): - a: float + a: bytes b: NotRequired[bool] def capybara(i: int, seq: Sequence[int], td: TD, s: str): @@ -1271,9 +1271,9 @@ def capybara(i: int, seq: Sequence[int], td: TD, s: str): d4 = {**d3, **td} assert_is_value(d4[1], KnownValue(1)) - assert_is_value(d4["a"], TypedValue(float)) + assert_is_value(d4["a"], TypedValue(bytes)) assert_is_value(d4["b"], KnownValue(2) | TypedValue(bool)) - assert_is_value(d4[s], TypedValue(float) | KnownValue(2) | TypedValue(bool)) + assert_is_value(d4[s], TypedValue(bytes) | KnownValue(2) | TypedValue(bool)) @assert_passes() def test(self): diff --git a/pycroscope/test_name_check_visitor.py b/pycroscope/test_name_check_visitor.py index ed07e608..d363e5d8 100644 --- a/pycroscope/test_name_check_visitor.py +++ b/pycroscope/test_name_check_visitor.py @@ -627,14 +627,14 @@ def test_metaclass_method(self): from typing import Type class EnumMeta(type): - def __getitem__(self, x: str) -> float: - return 42.0 + def __getitem__(self, x: str) -> bytes: + return b"hi" class Enum(metaclass=EnumMeta): pass def capybara(enum: Type[Enum]) -> None: - assert_is_value(enum["x"], TypedValue(float)) + assert_is_value(enum["x"], TypedValue(bytes)) @assert_passes() def test_type_union(self): @@ -1510,14 +1510,14 @@ class MyMapping: def keys(self) -> List[bool]: raise NotImplementedError - def __getitem__(self, key: bool) -> float: + def __getitem__(self, key: bool) -> bytes: raise NotImplementedError def capybara(m: MyMapping): assert_is_value( {**m}, DictIncompleteValue( - dict, [KVPair(TypedValue(bool), TypedValue(float), is_many=True)] + dict, [KVPair(TypedValue(bool), TypedValue(bytes), is_many=True)] ), ) @@ -1761,11 +1761,11 @@ def capybara() -> None: assert_is_value(x, KnownValue(3)) y: int = 3 assert_is_value(y, TypedValue(int)) - z: float + z: bytes print(z) # E: undefined_name - y: float = 4.0 # E: already_declared - assert_is_value(y, TypedValue(float)) + y: bytes = b"ytes" # E: already_declared + assert_is_value(y, TypedValue(bytes)) @assert_passes() def test_final(self): diff --git a/pycroscope/test_operations.py b/pycroscope/test_operations.py index 7c90b4e4..3453ab58 100644 --- a/pycroscope/test_operations.py +++ b/pycroscope/test_operations.py @@ -140,8 +140,8 @@ def capybara(x): assert_is_value(1 + int(x), TypedValue(int)) assert_is_value(3 * int(x), TypedValue(int)) assert_is_value("foo" + str(x), TypedValue(str)) - assert_is_value(1 + float(x), TypedValue(float)) - assert_is_value(1.0 + int(x), TypedValue(float)) + assert_is_value(1 + float(x), TypedValue(float) | TypedValue(int)) + assert_is_value(1.0 + int(x), TypedValue(float) | TypedValue(int)) assert_is_value(3 * 3.0 + 1, KnownValue(10.0)) @assert_passes() @@ -193,7 +193,7 @@ def capybara(): @assert_passes() def test_int_float_product(self): def capybara(f: float, i: int): - assert_is_value(i * f, TypedValue(float)) + assert_is_value(i * f, TypedValue(float) | TypedValue(int)) @assert_passes() def test_contains(self): @@ -233,7 +233,9 @@ def __eq__(self, other: int) -> float: return 3.14 def capybara(x: X): - assert_is_value(x == 1, TypedValue(float), skip_annotated=True) + assert_is_value( + x == 1, TypedValue(float) | TypedValue(int), skip_annotated=True + ) assert_is_value(x == "x", TypedValue(bool), skip_annotated=True) class Container: diff --git a/pycroscope/test_signature.py b/pycroscope/test_signature.py index c7bba9a7..b90fbdb8 100644 --- a/pycroscope/test_signature.py +++ b/pycroscope/test_signature.py @@ -1137,10 +1137,12 @@ def capybara(): print("x", file="not a file") # E: incompatible_argument def pacarana(f: float): + # https://github.com/python/cpython/issues/120080 + assert isinstance(f, float) assert_is_value(f.__round__(), TypedValue(int)) assert_is_value(f.__round__(None), TypedValue(int)) f.__round__(ndigits=None) # E: incompatible_call - assert_is_value(f.__round__(1), TypedValue(float)) + assert_is_value(f.__round__(1), TypedValue(float) | TypedValue(int)) @assert_passes() def test_runtime(self): diff --git a/pycroscope/test_type_evaluation.py b/pycroscope/test_type_evaluation.py index a9a587f2..f19414ae 100644 --- a/pycroscope/test_type_evaluation.py +++ b/pycroscope/test_type_evaluation.py @@ -113,11 +113,11 @@ def compare_evaluated(x: object) -> Union[int, str, float]: def capybara(unannotated): assert_is_value(compare_evaluated(None), TypedValue(str)) - assert_is_value(compare_evaluated(1), TypedValue(float)) + assert_is_value(compare_evaluated(1), TypedValue(float) | TypedValue(int)) assert_is_value(compare_evaluated("x"), TypedValue(int)) assert_is_value( compare_evaluated(None if unannotated else 1), - TypedValue(str) | TypedValue(float), + TypedValue(str) | TypedValue(float) | TypedValue(int), ) @assert_passes() diff --git a/pycroscope/test_typeshed.py b/pycroscope/test_typeshed.py index 5ac086e5..a744d168 100644 --- a/pycroscope/test_typeshed.py +++ b/pycroscope/test_typeshed.py @@ -56,7 +56,7 @@ def test_types(self): import math from typing import Container - assert_is_value(math.exp(1.0), TypedValue(float)) + assert_is_value(math.exp(1.0), TypedValue(float) | TypedValue(int)) assert_is_value("".isspace(), TypedValue(bool)) def capybara(x: Container[int]) -> None: @@ -206,7 +206,7 @@ def test_get_attribute(self) -> None: { "a": TypedDictEntry(TypedValue(int)), "b": TypedDictEntry(TypedValue(str)), - "c": TypedDictEntry(TypedValue(float)), + "c": TypedDictEntry(TypedValue(float) | TypedValue(int)), } ), } diff --git a/pycroscope/test_typevar.py b/pycroscope/test_typevar.py index a5a58b0f..a1dc4e9b 100644 --- a/pycroscope/test_typevar.py +++ b/pycroscope/test_typevar.py @@ -316,7 +316,7 @@ def f(x: T) -> T: def capybara(si: SupportsIndex): assert_is_value(f(1), TypedValue(int)) assert_is_value(f(si), TypedValue(SupportsIndex)) - assert_is_value(f(1.0), TypedValue(float)) + assert_is_value(f(1.0), TypedValue(float) | TypedValue(int)) @assert_passes() def test_lots_of_constraints(self): diff --git a/pycroscope/test_value.py b/pycroscope/test_value.py index 0a4882be..feeb9490 100644 --- a/pycroscope/test_value.py +++ b/pycroscope/test_value.py @@ -163,10 +163,10 @@ def test_typed_value() -> None: float_val = TypedValue(float) assert str(float_val) == "float" assert_can_assign(float_val, KnownValue(1.0)) - assert_can_assign(float_val, KnownValue(1)) + assert_cannot_assign(float_val, KnownValue(1)) assert_cannot_assign(float_val, KnownValue("")) assert_can_assign(float_val, TypedValue(float)) - assert_can_assign(float_val, TypedValue(int)) + assert_cannot_assign(float_val, TypedValue(int)) assert_cannot_assign(float_val, TypedValue(str)) assert_can_assign(float_val, TypedValue(mock.Mock)) @@ -222,7 +222,7 @@ def test_subclass_value() -> None: assert TypedValue(str) == val.typ assert val.is_type(str) assert not val.is_type(int) - val = SubclassValue(TypedValue(float)) + val = SubclassValue(TypedValue(float)) | SubclassValue(TypedValue(int)) assert_can_assign(val, KnownValue(int)) assert_can_assign(val, SubclassValue(TypedValue(int))) @@ -606,12 +606,20 @@ def test_new_type_value() -> None: assert_cannot_assign(nt1_val, TypedValue(Capybara)) assert_cannot_assign(nt1_val, KnownValue(Capybara.hydrochaeris)) + assert_can_assign(nt1_val, AnyValue(AnySource.marker)) + assert_can_assign(AnyValue(AnySource.marker), nt1_val) + def test_annotated_value() -> None: tv_int = TypedValue(int) assert_can_assign(AnnotatedValue(tv_int, [tv_int]), tv_int) assert_can_assign(tv_int, AnnotatedValue(tv_int, [tv_int])) + union = TypedValue(int) | TypedValue(float) + annotated = AnnotatedValue(union, [KnownValue(1)]) + assert_can_assign(annotated, union) + assert_can_assign(union, annotated) + class A: pass diff --git a/pycroscope/type_object.py b/pycroscope/type_object.py index 7865bf5a..2ca74040 100644 --- a/pycroscope/type_object.py +++ b/pycroscope/type_object.py @@ -78,13 +78,6 @@ def __post_init__(self) -> None: self.is_final = True self.is_thrift_enum = hasattr(self.typ, "_VALUES_TO_NAMES") self.base_classes |= set(get_mro(self.typ)) - # As a special case, the Python type system treats int as - # a subtype of float, and both int and float as subtypes of complex. - if self.typ is int or safe_in(int, self.base_classes): - self.artificial_bases.add(float) - self.artificial_bases.add(complex) - if self.typ is float or safe_in(float, self.base_classes): - self.artificial_bases.add(complex) if self.is_thrift_enum: self.artificial_bases.add(int) self.base_classes |= self.artificial_bases