Skip to content

Commit 2a5a88d

Browse files
Implement float/int special case through unions (#44)
Implements python/typing#1748.
1 parent 9a08c33 commit 2a5a88d

16 files changed

+206
-95
lines changed

docs/changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
- Change implementation of implicit int/float and float/complex promotion
6+
in accordance with https://github.com/python/typing/pull/1748. Now,
7+
annotations of `float` implicitly mean `float | int`.
8+
- Fix assignability for certain combinations of unions, `Annotated`, and `NewType`.
59
- Reduce more uninhabited intersections to `Never`
610

711
## Version 0.2.0 (June 26, 2025)

pycroscope/annotations.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1342,6 +1342,10 @@ def _maybe_typed_value(val: Union[type, str]) -> Value:
13421342
return HashableProtoValue
13431343
elif val is Callable or is_typing_name(val, "Callable"):
13441344
return CallableValue(ANY_SIGNATURE)
1345+
elif val is float:
1346+
return TypedValue(float) | TypedValue(int)
1347+
elif val is complex:
1348+
return TypedValue(complex) | TypedValue(float) | TypedValue(int)
13451349
return TypedValue(val)
13461350

13471351

pycroscope/arg_spec.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,17 +1062,22 @@ def _get_generic_bases_cached(self, typ: Union[type, str]) -> GenericBases:
10621062
assert isinstance(
10631063
typ, type
10641064
), f"failed to extract typeshed bases for {typ!r}"
1065-
bases = [
1066-
type_from_runtime(base, ctx=self.default_context)
1067-
for base in self.get_runtime_bases(typ)
1068-
]
1065+
bases = [self._type_from_base(base) for base in self.get_runtime_bases(typ)]
10691066
generic_bases = self._extract_bases(typ, bases)
10701067
assert (
10711068
generic_bases is not None
10721069
), f"failed to extract runtime bases from {typ}"
10731070
self.generic_bases_cache[typ] = generic_bases
10741071
return generic_bases
10751072

1073+
def _type_from_base(self, base: object) -> Value:
1074+
# Avoid promoting float to float|int here.
1075+
if base is float:
1076+
return TypedValue(float)
1077+
elif base is complex:
1078+
return TypedValue(complex)
1079+
return type_from_runtime(base, ctx=self.default_context)
1080+
10761081
def _extract_bases(
10771082
self, typ: Union[type, str], bases: Optional[Sequence[Value]]
10781083
) -> Optional[GenericBases]:

pycroscope/relations.py

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,15 @@ def _has_relation(
191191
right: GradualType,
192192
relation: Literal[Relation.SUBTYPE, Relation.ASSIGNABLE],
193193
ctx: CanAssignContext,
194+
*,
195+
original_left: Optional[GradualType] = None,
196+
original_right: Optional[GradualType] = None,
194197
) -> CanAssign:
198+
if original_right is None:
199+
original_right = right
200+
if original_left is None:
201+
original_left = left
202+
195203
# TypeVarValue
196204
if isinstance(left, TypeVarValue):
197205
if left == right:
@@ -224,7 +232,7 @@ def _has_relation(
224232
# AnnotatedValue
225233
if isinstance(left, AnnotatedValue):
226234
left_inner = gradualize(left.value)
227-
can_assign = _has_relation(left_inner, right, relation, ctx)
235+
can_assign = _has_relation(left_inner, right, relation, ctx, original_left=left)
228236
if isinstance(can_assign, CanAssignError):
229237
return can_assign
230238
bounds_maps = [can_assign]
@@ -234,9 +242,11 @@ def _has_relation(
234242
return custom_can_assign
235243
bounds_maps.append(custom_can_assign)
236244
return unify_bounds_maps(bounds_maps)
237-
if isinstance(right, AnnotatedValue) and not isinstance(left, MultiValuedValue):
245+
if isinstance(right, AnnotatedValue):
238246
right_inner = gradualize(right.value)
239-
can_assign = _has_relation(left, right_inner, relation, ctx)
247+
can_assign = _has_relation(
248+
left, right_inner, relation, ctx, original_right=right
249+
)
240250
if isinstance(can_assign, CanAssignError):
241251
return can_assign
242252
bounds_maps = [can_assign]
@@ -247,19 +257,34 @@ def _has_relation(
247257
bounds_maps.append(custom_can_assign)
248258
return unify_bounds_maps(bounds_maps)
249259

260+
# NewTypeValue
261+
if isinstance(left, NewTypeValue):
262+
if isinstance(right, NewTypeValue):
263+
if left.newtype is right.newtype:
264+
return {}
265+
else:
266+
return CanAssignError(f"{right} is not {relation.description} {left}")
267+
elif isinstance(right, AnyValue):
268+
pass
269+
else:
270+
return CanAssignError(f"{right} is not {relation.description} {left}")
271+
if isinstance(right, NewTypeValue):
272+
right_inner = gradualize(right.value)
273+
return _has_relation(left, right_inner, relation, ctx, original_right=right)
274+
250275
# IntersectionValue
251276
if isinstance(left, IntersectionValue):
252277
# Try to simplify first
253278
left = intersect_multi(left.vals, ctx)
254279
if not isinstance(left, IntersectionValue):
255-
return _has_relation(left, right, relation, ctx)
280+
return _has_relation(left, original_right, relation, ctx)
256281
if isinstance(right, IntersectionValue):
257282
right = intersect_multi(right.vals, ctx)
258283
# Must be a subtype of all the members
259284
bounds_maps = []
260285
errors = []
261286
for val in left.vals:
262-
can_assign = _has_relation(gradualize(val), right, relation, ctx)
287+
can_assign = _has_relation(gradualize(val), original_right, relation, ctx)
263288
if isinstance(can_assign, CanAssignError):
264289
errors.append(can_assign)
265290
else:
@@ -272,12 +297,12 @@ def _has_relation(
272297
if isinstance(right, IntersectionValue):
273298
right = intersect_multi(right.vals, ctx)
274299
if not isinstance(right, IntersectionValue):
275-
return _has_relation(left, right, relation, ctx)
300+
return _has_relation(original_left, right, relation, ctx)
276301
# At least one member must be a subtype
277302
bounds_maps = []
278303
errors = []
279304
for val in right.vals:
280-
can_assign = _has_relation(left, gradualize(val), relation, ctx)
305+
can_assign = _has_relation(original_left, gradualize(val), relation, ctx)
281306
if isinstance(can_assign, CanAssignError):
282307
errors.append(can_assign)
283308
else:
@@ -304,7 +329,7 @@ def _has_relation(
304329
errors = []
305330
for val in left.vals:
306331
val = gradualize(val)
307-
can_assign = _has_relation(val, right, relation, ctx)
332+
can_assign = _has_relation(val, original_right, relation, ctx)
308333
if isinstance(can_assign, CanAssignError):
309334
errors.append(can_assign)
310335
else:
@@ -326,7 +351,7 @@ def _has_relation(
326351
bounds_maps = []
327352
for val in right.vals:
328353
val = gradualize(val)
329-
can_assign = _has_relation(left, val, relation, ctx)
354+
can_assign = _has_relation(original_left, val, relation, ctx)
330355
if isinstance(can_assign, CanAssignError):
331356
# Adding an additional layer here isn't helpful
332357
return can_assign
@@ -359,6 +384,7 @@ def _has_relation(
359384
return {} # Any is assignable to everything
360385
else:
361386
assert_never(relation)
387+
assert not isinstance(left, NewTypeValue)
362388

363389
# SyntheticModuleValue
364390
if isinstance(left, SyntheticModuleValue):
@@ -395,19 +421,6 @@ def _has_relation(
395421
if isinstance(right, ParamSpecKwargsValue):
396422
return has_relation(left, right.get_fallback_value(), relation, ctx)
397423

398-
# NewTypeValue
399-
if isinstance(left, NewTypeValue):
400-
if isinstance(right, NewTypeValue):
401-
if left.newtype is right.newtype:
402-
return {}
403-
else:
404-
return CanAssignError(f"{right} is not {relation.description} {left}")
405-
else:
406-
return CanAssignError(f"{right} is not {relation.description} {left}")
407-
if isinstance(right, NewTypeValue):
408-
right_inner = gradualize(right.value)
409-
return _has_relation(left, right_inner, relation, ctx)
410-
411424
# UnboundMethodValue
412425
if isinstance(left, UnboundMethodValue):
413426
if isinstance(right, UnboundMethodValue) and left == right:

0 commit comments

Comments
 (0)