Skip to content

Commit 77cfdc2

Browse files
committed
1 parent 8dcd899 commit 77cfdc2

3 files changed

Lines changed: 163 additions & 86 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# Unreleased
2+
3+
- Backport `Protocol.__init__` behaviour from python 3.11. (see
4+
python/cpython#31628, by Adrian Garcia Badaracco). Patch by James
5+
Hilton-Balfe (@Gobot1234).
6+
17
# Release 4.5.0 (February 14, 2023)
28

39
- Runtime support for PEP 702, adding `typing_extensions.deprecated`. Patch

‎src/test_typing_extensions.py‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1427,6 +1427,32 @@ class PG(Protocol[T]): pass
14271427
class CG(PG[T]): pass
14281428
self.assertIsInstance(CG[int](), CG)
14291429

1430+
def test_protocol_defining_init_does_not_get_overridden(self):
1431+
# check that P.__init__ doesn't get clobbered
1432+
# see https://bugs.python.org/issue44807
1433+
1434+
class P(Protocol):
1435+
x: int
1436+
def __init__(self, x: int) -> None:
1437+
self.x = x
1438+
class C: pass
1439+
1440+
c = C()
1441+
P.__init__(c, 1)
1442+
self.assertEqual(c.x, 1)
1443+
1444+
def test_concrete_class_inheriting_init_from_protocol(self):
1445+
class P(Protocol):
1446+
x: int
1447+
def __init__(self, x: int) -> None:
1448+
self.x = x
1449+
1450+
class C(P): pass
1451+
1452+
c = C(1)
1453+
self.assertIsInstance(c, C)
1454+
self.assertEqual(c.x, 1)
1455+
14301456
def test_cannot_instantiate_abstract(self):
14311457
@runtime
14321458
class P(Protocol):
@@ -3302,7 +3328,7 @@ def test_typing_extensions_defers_when_possible(self):
33023328
if sys.version_info < (3, 10):
33033329
exclude |= {'get_args', 'get_origin'}
33043330
if sys.version_info < (3, 11):
3305-
exclude |= {'final', 'NamedTuple', 'Any'}
3331+
exclude |= {'final', 'NamedTuple', 'Any', 'Protocol'}
33063332
for item in typing_extensions.__all__:
33073333
if item not in exclude and hasattr(typing, item):
33083334
self.assertIs(

‎src/typing_extensions.py‎

Lines changed: 130 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -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

147154
NoReturn = 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+
464471
else:
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):
22292280
if sys.version_info >= (3, 11):
22302281
NamedTuple = typing.NamedTuple
22312282
else:
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

Comments
 (0)