Skip to content
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions pycroscope/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
13 changes: 9 additions & 4 deletions pycroscope/arg_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -1062,17 +1062,22 @@ 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
), f"failed to extract runtime bases from {typ}"
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]:
Expand Down
57 changes: 35 additions & 22 deletions pycroscope/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
Loading
Loading