|
17 | 17 | import os |
18 | 18 | import pkgutil |
19 | 19 | import re |
| 20 | +import struct |
20 | 21 | import symtable |
21 | 22 | import sys |
22 | 23 | import traceback |
@@ -465,6 +466,82 @@ class SubClass(runtime): # type: ignore[misc] |
465 | 466 | stub_desc=repr(stub), |
466 | 467 | ) |
467 | 468 |
|
| 469 | +SIZEOF_PYOBJECT = struct.calcsize("P") |
| 470 | + |
| 471 | + |
| 472 | +def _shape_differs(t1: type[object], t2: type[object]) -> bool: |
| 473 | + """Check whether two types differ in shape. |
| 474 | +
|
| 475 | + Mirrors the shape_differs() function in typeobject.c in CPython.""" |
| 476 | + if sys.version_info >= (3, 12): |
| 477 | + return ( |
| 478 | + t1.__basicsize__ != t2.__basicsize__ or t1.__itemsize__ != t2.__itemsize__ |
| 479 | + ) |
| 480 | + else: |
| 481 | + # CPython had more complicated logic before 3.12: |
| 482 | + # https://github.com/python/cpython/blob/f3c6f882cddc8dc30320d2e73edf019e201394fc/Objects/typeobject.c#L2224 |
| 483 | + # We attempt to mirror it here well enough to support the most common cases. |
| 484 | + if t1.__itemsize__ or t2.__itemsize__: |
| 485 | + return ( |
| 486 | + t1.__basicsize__ != t2.__basicsize__ |
| 487 | + or t1.__itemsize__ != t2.__itemsize__ |
| 488 | + ) |
| 489 | + t_size = t1.__basicsize__ |
| 490 | + if ( |
| 491 | + # TODO: better understanding of attributes of type objects |
| 492 | + not t2.__weakrefoffset__ # static analysis: ignore[value_always_true] |
| 493 | + and t1.__weakrefoffset__ + SIZEOF_PYOBJECT == t_size |
| 494 | + ): |
| 495 | + t_size -= SIZEOF_PYOBJECT |
| 496 | + if ( |
| 497 | + not t2.__dictoffset__ # static analysis: ignore[value_always_true] |
| 498 | + and t1.__dictoffset__ + SIZEOF_PYOBJECT == t_size |
| 499 | + ): |
| 500 | + t_size -= SIZEOF_PYOBJECT |
| 501 | + if ( |
| 502 | + not t2.__weakrefoffset__ # static analysis: ignore[value_always_true] |
| 503 | + and t2.__weakrefoffset__ == t_size |
| 504 | + ): |
| 505 | + t_size -= SIZEOF_PYOBJECT |
| 506 | + return t_size != t2.__basicsize__ |
| 507 | + |
| 508 | + |
| 509 | +def _is_disjoint_base(typ: type[object]) -> bool: |
| 510 | + """Return whether a type is a disjoint base at runtime, mirroring CPython's logic in typeobject.c. |
| 511 | +
|
| 512 | + See PEP 800.""" |
| 513 | + if typ is object: |
| 514 | + return True |
| 515 | + base = typ.__base__ |
| 516 | + assert base is not None, f"Type {typ} has no base" |
| 517 | + return _shape_differs(typ, base) |
| 518 | + |
| 519 | + |
| 520 | + |
| 521 | +def _verify_disjoint_base( |
| 522 | + stub: nodes.TypeInfo, runtime: type[object], object_path: list[str] |
| 523 | +) -> Iterator[Error]: |
| 524 | + # If it's final, doesn't matter whether it's a disjoint base or not |
| 525 | + if stub.is_final: |
| 526 | + return |
| 527 | + is_disjoint_runtime = _is_disjoint_base(runtime) |
| 528 | + if is_disjoint_runtime and not stub.is_disjoint_base: |
| 529 | + yield Error( |
| 530 | + object_path, |
| 531 | + "is a disjoint base at runtime, but isn't marked with @disjoint_base in the stub", |
| 532 | + stub, |
| 533 | + runtime, |
| 534 | + stub_desc=repr(stub), |
| 535 | + ) |
| 536 | + elif not is_disjoint_runtime and stub.is_disjoint_base: |
| 537 | + yield Error( |
| 538 | + object_path, |
| 539 | + "is marked with @disjoint_base in the stub, but isn't a disjoint base at runtime", |
| 540 | + stub, |
| 541 | + runtime, |
| 542 | + stub_desc=repr(stub), |
| 543 | + ) |
| 544 | + |
468 | 545 |
|
469 | 546 | def _verify_metaclass( |
470 | 547 | stub: nodes.TypeInfo, runtime: type[Any], object_path: list[str], *, is_runtime_typeddict: bool |
@@ -534,6 +611,7 @@ def verify_typeinfo( |
534 | 611 | return |
535 | 612 |
|
536 | 613 | yield from _verify_final(stub, runtime, object_path) |
| 614 | + yield from _verify_disjoint_base(stub, runtime, object_path) |
537 | 615 | is_runtime_typeddict = stub.typeddict_type is not None and is_typeddict(runtime) |
538 | 616 | yield from _verify_metaclass( |
539 | 617 | stub, runtime, object_path, is_runtime_typeddict=is_runtime_typeddict |
|
0 commit comments