@@ -93,6 +93,8 @@ def _check_generic(cls, parameters, elen=_marker):
9393 """Check correct count for parameters of a generic cls (internal helper).
9494 This gives a nice error message in case of count mismatch.
9595 """
96+ if cls is Protocol and not elen :
97+ return
9698 if not elen :
9799 raise TypeError (f"{ cls } is not a generic class" )
98100 if elen is _marker :
@@ -143,6 +145,11 @@ def _collect_type_vars(types, typevar_types=None):
143145 tvars .extend ([t for t in t .__parameters__ if t not in tvars ])
144146 return tuple (tvars )
145147
148+ def _caller (depth = 1 , default = '__main__' ):
149+ try :
150+ return sys ._getframe (depth + 1 ).f_globals .get ('__name__' , default )
151+ except (AttributeError , ValueError ): # For platforms without _getframe()
152+ return None
146153
147154NoReturn = typing .NoReturn
148155
@@ -457,162 +464,206 @@ def _maybe_adjust_parameters(cls):
457464 cls .__parameters__ = tuple (tvars )
458465
459466
460- # 3.8 +
461- if hasattr ( typing , 'Protocol' ):
467+ # 3.11 +
468+ if sys . version_info >= ( 3 , 11 ): # 3.8 has Protocol but it doesn't preserve __init__
462469 Protocol = typing .Protocol
463- # 3.7
470+
464471else :
472+ _TYPING_INTERNALS = ['__parameters__' , '__orig_bases__' , '__orig_class__' ,
473+ '_is_protocol' , '_is_runtime_protocol' ]
474+
475+ _SPECIAL_NAMES = ['__abstractmethods__' , '__annotations__' , '__dict__' , '__doc__' ,
476+ '__init__' , '__module__' , '__new__' , '__slots__' ,
477+ '__subclasshook__' , '__weakref__' , '__class_getitem__' ]
478+
479+ # These special attributes will be not collected as protocol members.
480+ _EXCLUDED_ATTRIBUTES = _TYPING_INTERNALS + _SPECIAL_NAMES + ['_MutableMapping__marker' ]
481+
482+
483+ def _get_protocol_attrs (cls ):
484+ """Collect protocol members from a protocol class objects.
485+ This includes names actually defined in the class dictionary, as well
486+ as names that appear in annotations. Special names (above) are skipped.
487+ """
488+ attrs = set ()
489+ for base in cls .__mro__ [:- 1 ]: # without object
490+ if base .__name__ in ('Protocol' , 'Generic' ):
491+ continue
492+ annotations = getattr (base , '__annotations__' , {})
493+ for attr in list (base .__dict__ .keys ()) + list (annotations .keys ()):
494+ if not attr .startswith ('_abc_' ) and attr not in _EXCLUDED_ATTRIBUTES :
495+ attrs .add (attr )
496+ return attrs
497+
498+
499+ def _is_callable_members_only (cls ):
500+ # PEP 544 prohibits using issubclass() with protocols that have non-method members.
501+ return all (callable (getattr (cls , attr , None )) for attr in _get_protocol_attrs (cls ))
465502
466- def _no_init (self , * args , ** kwargs ):
467- if type (self )._is_protocol :
503+
504+ def _no_init_or_replace_init (self , * args , ** kwargs ):
505+ cls = type (self )
506+
507+ if cls ._is_protocol :
468508 raise TypeError ('Protocols cannot be instantiated' )
469509
470- class _ProtocolMeta (abc .ABCMeta ): # noqa: B024
471- # This metaclass is a bit unfortunate and exists only because of the lack
472- # of __instancehook__.
510+ # Already using a custom `__init__`. No need to calculate correct
511+ # `__init__` to call. This can lead to RecursionError. See bpo-45121.
512+ if cls .__init__ is not _no_init_or_replace_init :
513+ return
514+
515+ # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
516+ # The first instantiation of the subclass will call `_no_init_or_replace_init` which
517+ # searches for a proper new `__init__` in the MRO. The new `__init__`
518+ # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
519+ # instantiation of the protocol subclass will thus use the new
520+ # `__init__` and no longer call `_no_init_or_replace_init`.
521+ for base in cls .__mro__ :
522+ init = base .__dict__ .get ('__init__' , _no_init_or_replace_init )
523+ if init is not _no_init_or_replace_init :
524+ cls .__init__ = init
525+ break
526+ else :
527+ # should not happen
528+ cls .__init__ = object .__init__
529+
530+ cls .__init__ (self , * args , ** kwargs )
531+
532+
533+ def _allow_reckless_class_checks (depth = 3 ):
534+ """Allow instance and class checks for special stdlib modules.
535+ The abc and functools modules indiscriminately call isinstance() and
536+ issubclass() on the whole MRO of a user class, which may contain protocols.
537+ """
538+ return _caller (depth ) in {'abc' , 'functools' , None }
539+
540+
541+ _PROTO_ALLOWLIST = {
542+ 'collections.abc' : [
543+ 'Callable' , 'Awaitable' , 'Iterable' , 'Iterator' , 'AsyncIterable' ,
544+ 'Hashable' , 'Sized' , 'Container' , 'Collection' , 'Reversible' ,
545+ ],
546+ 'contextlib' : ['AbstractContextManager' , 'AbstractAsyncContextManager' ],
547+ }
548+
549+
550+ class _ProtocolMeta (abc .ABCMeta ):
551+ # This metaclass is really unfortunate and exists only because of
552+ # the lack of __instancehook__.
473553 def __instancecheck__ (cls , instance ):
474554 # We need this method for situations where attributes are
475555 # assigned in __init__.
556+ if (
557+ getattr (cls , '_is_protocol' , False ) and
558+ not getattr (cls , '_is_runtime_protocol' , False ) and
559+ not _allow_reckless_class_checks (depth = 2 )
560+ ):
561+ raise TypeError ("Instance and class checks can only be used with"
562+ " @runtime_checkable protocols" )
563+
476564 if ((not getattr (cls , '_is_protocol' , False ) or
477- _is_callable_members_only (cls )) and
565+ _is_callable_members_only (cls )) and
478566 issubclass (instance .__class__ , cls )):
479567 return True
480568 if cls ._is_protocol :
481569 if all (hasattr (instance , attr ) and
482- (not callable (getattr (cls , attr , None )) or
570+ # All *methods* can be blocked by setting them to None.
571+ (not callable (getattr (cls , attr , None )) or
483572 getattr (instance , attr ) is not None )
484- for attr in _get_protocol_attrs (cls )):
573+ for attr in _get_protocol_attrs (cls )):
485574 return True
486575 return super ().__instancecheck__ (instance )
487576
488- class Protocol (metaclass = _ProtocolMeta ):
489- # There is quite a lot of overlapping code with typing.Generic.
490- # Unfortunately it is hard to avoid this while these live in two different
491- # modules. The duplicated code will be removed when Protocol is moved to typing.
492- """Base class for protocol classes. Protocol classes are defined as::
493577
578+ class Protocol (typing .Generic , metaclass = _ProtocolMeta ):
579+ """Base class for protocol classes.
580+ Protocol classes are defined as::
494581 class Proto(Protocol):
495582 def meth(self) -> int:
496583 ...
497-
498584 Such classes are primarily used with static type checkers that recognize
499585 structural subtyping (static duck-typing), for example::
500-
501586 class C:
502587 def meth(self) -> int:
503588 return 0
504-
505589 def func(x: Proto) -> int:
506590 return x.meth()
507-
508591 func(C()) # Passes static type check
509-
510592 See PEP 544 for details. Protocol classes decorated with
511- @typing_extensions.runtime act as simple-minded runtime protocol that checks
593+ @typing.runtime_checkable act as simple-minded runtime protocols that check
512594 only the presence of given attributes, ignoring their type signatures.
513-
514595 Protocol classes can be generic, they are defined as::
515-
516596 class GenProto(Protocol[T]):
517597 def meth(self) -> T:
518598 ...
519599 """
520600 __slots__ = ()
521601 _is_protocol = True
522-
523- def __new__ (cls , * args , ** kwds ):
524- if cls is Protocol :
525- raise TypeError ("Type Protocol cannot be instantiated; "
526- "it can only be used as a base class" )
527- return super ().__new__ (cls )
528-
529- @typing ._tp_cache
530- def __class_getitem__ (cls , params ):
531- if not isinstance (params , tuple ):
532- params = (params ,)
533- if not params and cls is not typing .Tuple :
534- raise TypeError (
535- f"Parameter list to { cls .__qualname__ } [...] cannot be empty" )
536- msg = "Parameters to generic types must be types."
537- params = tuple (typing ._type_check (p , msg ) for p in params ) # noqa
538- if cls is Protocol :
539- # Generic can only be subscripted with unique type variables.
540- if not all (isinstance (p , typing .TypeVar ) for p in params ):
541- i = 0
542- while isinstance (params [i ], typing .TypeVar ):
543- i += 1
544- raise TypeError (
545- "Parameters to Protocol[...] must all be type variables."
546- f" Parameter { i + 1 } is { params [i ]} " )
547- if len (set (params )) != len (params ):
548- raise TypeError (
549- "Parameters to Protocol[...] must all be unique" )
550- else :
551- # Subscripting a regular Generic subclass.
552- _check_generic (cls , params , len (cls .__parameters__ ))
553- return typing ._GenericAlias (cls , params )
602+ _is_runtime_protocol = False
554603
555604 def __init_subclass__ (cls , * args , ** kwargs ):
556- if '__orig_bases__' in cls .__dict__ :
557- error = typing .Generic in cls .__orig_bases__
558- else :
559- error = typing .Generic in cls .__bases__
560- if error :
561- raise TypeError ("Cannot inherit from plain Generic" )
562- _maybe_adjust_parameters (cls )
605+ super ().__init_subclass__ (* args , ** kwargs )
563606
564607 # Determine if this is a protocol or a concrete subclass.
565- if not cls .__dict__ .get ('_is_protocol' , None ):
608+ if not cls .__dict__ .get ('_is_protocol' , False ):
566609 cls ._is_protocol = any (b is Protocol for b in cls .__bases__ )
567610
568611 # Set (or override) the protocol subclass hook.
569612 def _proto_hook (other ):
570- if not cls .__dict__ .get ('_is_protocol' , None ):
613+ if not cls .__dict__ .get ('_is_protocol' , False ):
571614 return NotImplemented
615+
616+ # First, perform various sanity checks.
572617 if not getattr (cls , '_is_runtime_protocol' , False ):
573- if sys . _getframe ( 2 ). f_globals [ '__name__' ] in [ 'abc' , 'functools' ] :
618+ if _allow_reckless_class_checks () :
574619 return NotImplemented
575620 raise TypeError ("Instance and class checks can only be used with"
576- " @runtime protocols" )
621+ " @runtime_checkable protocols" )
577622 if not _is_callable_members_only (cls ):
578- if sys . _getframe ( 2 ). f_globals [ '__name__' ] in [ 'abc' , 'functools' ] :
623+ if _allow_reckless_class_checks () :
579624 return NotImplemented
580625 raise TypeError ("Protocols with non-method members"
581626 " don't support issubclass()" )
582627 if not isinstance (other , type ):
583- # Same error as for issubclass(1, int)
628+ # Same error message as for issubclass(1, int).
584629 raise TypeError ('issubclass() arg 1 must be a class' )
630+
631+ # Second, perform the actual structural compatibility check.
585632 for attr in _get_protocol_attrs (cls ):
586633 for base in other .__mro__ :
634+ # Check if the members appears in the class dictionary...
587635 if attr in base .__dict__ :
588636 if base .__dict__ [attr ] is None :
589637 return NotImplemented
590638 break
639+
640+ # ...or in annotations, if it is a sub-protocol.
591641 annotations = getattr (base , '__annotations__' , {})
592- if (isinstance (annotations , typing .Mapping ) and
642+ if (isinstance (annotations , collections . abc .Mapping ) and
593643 attr in annotations and
594- isinstance (other , _ProtocolMeta ) and
595- other ._is_protocol ):
644+ issubclass (other , typing .Generic ) and other ._is_protocol ):
596645 break
597646 else :
598647 return NotImplemented
599648 return True
649+
600650 if '__subclasshook__' not in cls .__dict__ :
601651 cls .__subclasshook__ = _proto_hook
602652
603- # We have nothing more to do for non-protocols.
653+ # We have nothing more to do for non-protocols...
604654 if not cls ._is_protocol :
605655 return
606656
607- # Check consistency of bases.
657+ # ... otherwise check consistency of bases, and prohibit instantiation .
608658 for base in cls .__bases__ :
609659 if not (base in (object , typing .Generic ) or
610- base .__module__ == 'collections.abc' and
611- base .__name__ in _PROTO_WHITELIST or
612- isinstance (base , _ProtocolMeta ) and base ._is_protocol ):
660+ base .__module__ in _PROTO_ALLOWLIST and
661+ base .__name__ in _PROTO_ALLOWLIST [ base . __module__ ] or
662+ issubclass (base , typing . Generic ) and base ._is_protocol ):
613663 raise TypeError ('Protocols can only inherit from other'
614- f' protocols, got { repr (base )} ' )
615- cls .__init__ = _no_init
664+ ' protocols, got %r' % base )
665+ if cls .__init__ is Protocol .__init__ :
666+ cls .__init__ = _no_init_or_replace_init
616667
617668
618669# 3.8+
@@ -2229,12 +2280,6 @@ def wrapper(*args, **kwargs):
22292280if sys .version_info >= (3 , 11 ):
22302281 NamedTuple = typing .NamedTuple
22312282else :
2232- def _caller ():
2233- try :
2234- return sys ._getframe (2 ).f_globals .get ('__name__' , '__main__' )
2235- except (AttributeError , ValueError ): # For platforms without _getframe()
2236- return None
2237-
22382283 def _make_nmtuple (name , types , module , defaults = ()):
22392284 fields = [n for n , t in types ]
22402285 annotations = {n : typing ._type_check (t , f"field { n } annotation must be a type" )
0 commit comments