|
5 | 5 | since these may assume that MROs are ready.
|
6 | 6 | """
|
7 | 7 |
|
8 |
| -from typing import cast, Optional, List, Sequence, Set, Iterable, TypeVar |
| 8 | +from typing import cast, Optional, List, Sequence, Set, Iterable, TypeVar, Tuple |
9 | 9 | from typing_extensions import Type as TypingType
|
10 | 10 | import sys
|
11 | 11 |
|
@@ -311,6 +311,15 @@ def callable_corresponding_argument(typ: CallableType,
|
311 | 311 | return by_name if by_name is not None else by_pos
|
312 | 312 |
|
313 | 313 |
|
| 314 | +def is_simple_literal(t: Type) -> bool: |
| 315 | + """ |
| 316 | + Whether a type is a simple enough literal to allow for fast set-based Union simplification |
| 317 | +
|
| 318 | + For now this means enuum or string |
| 319 | + """ |
| 320 | + return isinstance(t, LiteralType) and (t.fallback.type.is_enum or t.fallback.type.fullname == 'builtins.str') |
| 321 | + |
| 322 | + |
314 | 323 | def make_simplified_union(items: Sequence[Type],
|
315 | 324 | line: int = -1, column: int = -1,
|
316 | 325 | *, keep_erased: bool = False) -> ProperType:
|
@@ -344,35 +353,49 @@ def make_simplified_union(items: Sequence[Type],
|
344 | 353 | from mypy.subtypes import is_proper_subtype
|
345 | 354 |
|
346 | 355 | removed = set() # type: Set[int]
|
347 |
| - |
348 |
| - # Avoid slow nested for loop for Union of Literal of strings (issue #9169) |
349 |
| - if all((isinstance(item, LiteralType) and |
350 |
| - item.fallback.type.fullname == 'builtins.str') |
351 |
| - for item in items): |
352 |
| - seen = set() # type: Set[str] |
353 |
| - for index, item in enumerate(items): |
| 356 | + seen = set() # type: Set[Tuple[str, str]] |
| 357 | + |
| 358 | + # NB: having a separate fast path for Union of Literal and slow path for other things |
| 359 | + # would arguably be cleaner, however it breaks down when simplifying the Union of two |
| 360 | + # different enum types as try_expanding_enum_to_union works recursively and will |
| 361 | + # trigger intermediate simplifications that would render the fast path useless |
| 362 | + for i, item in enumerate(items): |
| 363 | + if i in removed: |
| 364 | + continue |
| 365 | + # Avoid slow nested for loop for Union of Literal of strings/enums (issue #9169) |
| 366 | + if is_simple_literal(item): |
354 | 367 | assert isinstance(item, LiteralType)
|
355 | 368 | assert isinstance(item.value, str)
|
356 |
| - if item.value in seen: |
357 |
| - removed.add(index) |
358 |
| - seen.add(item.value) |
| 369 | + k = (item.value, item.fallback.type.fullname) |
| 370 | + if k in seen: |
| 371 | + removed.add(i) |
| 372 | + continue |
359 | 373 |
|
360 |
| - else: |
361 |
| - for i, ti in enumerate(items): |
362 |
| - if i in removed: continue |
363 |
| - # Keep track of the truishness info for deleted subtypes which can be relevant |
364 |
| - cbt = cbf = False |
365 |
| - for j, tj in enumerate(items): |
366 |
| - if i != j and is_proper_subtype(tj, ti, keep_erased_types=keep_erased): |
367 |
| - # We found a redundant item in the union. |
368 |
| - removed.add(j) |
369 |
| - cbt = cbt or tj.can_be_true |
370 |
| - cbf = cbf or tj.can_be_false |
371 |
| - # if deleted subtypes had more general truthiness, use that |
372 |
| - if not ti.can_be_true and cbt: |
373 |
| - items[i] = true_or_false(ti) |
374 |
| - elif not ti.can_be_false and cbf: |
375 |
| - items[i] = true_or_false(ti) |
| 374 | + # NB: one would naively expect that it would be safe to skip the slow path |
| 375 | + # always for literals. One would be sorely mistaken. Indeed, some simplifications |
| 376 | + # such as that of None/Optional when strict optional is false, do require that we |
| 377 | + # proceed with the slow path. Thankfully, all literals will have the same subtype |
| 378 | + # relationship to non-literal types, so we only need to do that walk for the first |
| 379 | + # literal, which keeps the fast path fast even in the presence of a mixture of |
| 380 | + # literals and other types. |
| 381 | + safe_skip = len(seen) > 0 |
| 382 | + seen.add(k) |
| 383 | + if safe_skip: |
| 384 | + continue |
| 385 | + # Keep track of the truishness info for deleted subtypes which can be relevant |
| 386 | + cbt = cbf = False |
| 387 | + for j, tj in enumerate(items): |
| 388 | + # NB: we don't need to check literals as the fast path above takes care of that |
| 389 | + if i != j and not is_simple_literal(tj) and is_proper_subtype(tj, item, keep_erased_types=keep_erased): |
| 390 | + # We found a redundant item in the union. |
| 391 | + removed.add(j) |
| 392 | + cbt = cbt or tj.can_be_true |
| 393 | + cbf = cbf or tj.can_be_false |
| 394 | + # if deleted subtypes had more general truthiness, use that |
| 395 | + if not item.can_be_true and cbt: |
| 396 | + items[i] = true_or_false(item) |
| 397 | + elif not item.can_be_false and cbf: |
| 398 | + items[i] = true_or_false(item) |
376 | 399 |
|
377 | 400 | simplified_set = [items[i] for i in range(len(items)) if i not in removed]
|
378 | 401 | return UnionType.make_union(simplified_set, line, column)
|
|
0 commit comments