Skip to content

Treat Protocol bases as ABCMeta-providing when detecting abstract cla…#3683

Open
mikeleppane wants to merge 1 commit into
facebook:mainfrom
mikeleppane:fix/final-class-abstract-protocol-base-2797
Open

Treat Protocol bases as ABCMeta-providing when detecting abstract cla…#3683
mikeleppane wants to merge 1 commit into
facebook:mainfrom
mikeleppane:fix/final-class-abstract-protocol-base-2797

Conversation

@mikeleppane
Copy link
Copy Markdown

Fixes #2797

What

A @final class that inherits collections.abc.Sequence (or Mapping) without
implementing its abstract methods is now flagged, matching mypy, pyright, and ty.

from typing import final
from collections.abc import Sequence

@final
class A(Sequence[int]):  # before: no error
    ...                  # after:  Final class `A` cannot have unimplemented
                         #         abstract members: `__getitem__`, `__len__`

Instantiating such a class (A()) and the opt-in implicit-abstract-class
diagnostic now fire correctly too.

Why

In typeshed the small collections.abc ABCs (Reversible, Collection, …) are
declared Protocol, and Protocol is a special form (Protocol: _SpecialForm)
that carries no metaclass. pyrefly's extends_abc heuristic only recognized an
abc.ABC base or an ABCMeta metaclass, so it never saw the _ProtocolMeta(ABCMeta)
that a protocol subclass actually gets at runtime. The composite Sequence — a
plain class whose only bases are those protocols — was therefore misclassified as
non-abstract and skipped, so its @final subclasses inherited no abstract
obligation. (Direct protocol bases like class B(Collection, Reversible) already
worked; the gap was one level of indirection away.) mypy, pyright, and ty are the
conformance bar and all three error here.

How

  • pyrefly/lib/alt/class/class_metadata.rsextends_abc now treats an
    is_protocol() base as ABCMeta-providing (a protocol base ⇒ _ProtocolMeta
    ABCMeta-derived metaclass). This is the whole fix for the reported bug.
  • Same filecalculate_abstract_members short-circuits for is_typed_dict()
    classes. Recognizing Mapping as abstract otherwise leaked through the
    synthesized TypedDictFallback(Mapping, metaclass=ABCMeta) base and made every
    TypedDict look abstract; a TypedDict is a concrete dict and never is. Caught by
    mypy_primer on pandas-stubs, not the unit suite.
  • Deliberately untouched: the two abstract-member gates (already correct given a
    correct extends_abc) and the truthiness-redundancy consumer of extends_abc in
    expr.rs (widening it only suppresses more "always true" warnings on abstract
    types — conservative).

Test plan

Test (pyrefly/lib/test/abstract_methods.rs) Protects
test_final_class_over_sequence the repro: @final over Sequence errors
test_abstract_through_protocol_bases instantiation, Mapping, and a user Protocol reached via a plain intermediate
test_implicit_abstract_through_sequence opt-in implicit-abstract diagnostic on Sequence subclasses
test_concrete_subclass_of_sequence_is_clean implementing the members stays clean — no over-reporting
test_final_class_direct_protocol_bases_still_errors the pre-existing direct-protocol path is unchanged
test_typed_dict_is_not_abstract TypedDicts stay concrete (regression guard)
  • Full lib suite: 5340 passed, 0 failed.
  • Conformance: pass, no generated changes.
  • mypy_primer, wide scope (~37 projects incl. pandas-stubs, pandas, sympy, xarray): zero diff.
  • format + lint: clean.

…sses

A `@final` class inheriting `collections.abc.Sequence` without implementing
`__getitem__`/`__len__` was not flagged, though mypy, pyright, and ty all error.
In typeshed the small `collections.abc` ABCs (`Reversible`, `Collection`) are
`Protocol`s, and `Protocol` is a special form carrying no metaclass, so
`extends_abc` never saw the `_ProtocolMeta(ABCMeta)` that makes any protocol
subclass an ABC at runtime. The composite `Sequence` was thus misclassified as
non-abstract and skipped by the abstract-member machinery, hiding its
unimplemented members.

Make `extends_abc` treat an `is_protocol()` base as ABCMeta-providing, matching
the runtime metaclass. This propagates through the plain intermediate so the
final / instantiation / implicit-abstract checks fire correctly.

Recognizing `Mapping` as abstract exposed a second issue: every TypedDict is
modeled with a synthesized `TypedDictFallback(Mapping, metaclass=ABCMeta)` base
that never redeclares Mapping's methods, which would make all TypedDicts look
abstract. Short-circuit `calculate_abstract_members` for TypedDicts, which are
concrete dicts and never abstract.

Fixes facebook#2797
@meta-cla meta-cla Bot added the cla signed label Jun 4, 2026
@github-actions github-actions Bot added the size/m label Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

final class implementing Sequence shows no error when abstract method is not implemented

1 participant