From a613af69372d0b4682a7e95360ead6044c649d50 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 22 Aug 2021 16:58:56 +0800 Subject: [PATCH 1/8] Support issubclass for ClassVar data members --- Doc/library/typing.rst | 5 +++++ Doc/whatsnew/3.11.rst | 8 ++++++++ Lib/test/test_typing.py | 12 ++++++++++++ Lib/typing.py | 14 +++++++++++--- .../2021-08-22-16-58-02.bpo-44975.ub3Vxk.rst | 2 ++ 5 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-08-22-16-58-02.bpo-44975.ub3Vxk.rst diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 47d6c3a2e38985..4e535972fc91f6 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -1247,6 +1247,11 @@ These are not used in annotations. They are building blocks for creating generic .. versionadded:: 3.8 + .. versionchanged:: 3.11 + Protocols with data members annotated with :data:`ClassVar` now support + :func:`issubclass` checks. Subclasses must set these data members to pass. + + Other special directives """""""""""""""""""""""" diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 49b4364be9bd7f..b614c96d12f100 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -219,6 +219,14 @@ sqlite3 (Contributed by Erlend E. Aasland in :issue:`44688`.) +typing +------ + +* Runtime protocols with data members now support :func:`issubclass` as long + as those members are annotated with :data:`typing.ClassVar`. + (Contributed by Ken Jin in :issue:`44975`). + + Removed ======= * :class:`smtpd.MailmanProxy` is now removed as it is unusable without diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 3bd5894f425741..60c43929f1f0ac 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1610,6 +1610,18 @@ class P(Protocol): with self.assertRaisesRegex(TypeError, "@runtime_checkable"): isinstance(1, P) + def test_runtime_issubclass_with_classvar_data_members(self): + @runtime_checkable + class P(Protocol): + x: ClassVar[int] = 1 + + class C: pass + + class D: + x = 1 + self.assertNotIsSubclass(C, P) + self.assertIsSubclass(D, P) + class GenericTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index a283fe40d2d3ff..6c085b6e6cb9c1 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1396,8 +1396,15 @@ def _get_protocol_attrs(cls): def _is_callable_members_only(cls): + attr_names = _get_protocol_attrs(cls) + annotations = getattr(cls, '__annotations__', {}) # PEP 544 prohibits using issubclass() with protocols that have non-method members. - return all(callable(getattr(cls, attr, None)) for attr in _get_protocol_attrs(cls)) + for attr_name in attr_names: + attr = getattr(cls, attr_name, None) + if not (callable(attr) + or (getattr(annotations.get(attr_name), '__name__', None) == 'ClassVar')): + return False + return True def _no_init(self, *args, **kwargs): @@ -1511,8 +1518,9 @@ def _proto_hook(other): if not _is_callable_members_only(cls): if _allow_reckless_class_checks(): return NotImplemented - raise TypeError("Protocols with non-method members" - " don't support issubclass()") + raise TypeError("Protocol members must be methods or data" + " attributes annotated with ClassVar to support" + " issubclass()") if not isinstance(other, type): # Same error message as for issubclass(1, int). raise TypeError('issubclass() arg 1 must be a class') diff --git a/Misc/NEWS.d/next/Library/2021-08-22-16-58-02.bpo-44975.ub3Vxk.rst b/Misc/NEWS.d/next/Library/2021-08-22-16-58-02.bpo-44975.ub3Vxk.rst new file mode 100644 index 00000000000000..a1b96539594528 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-08-22-16-58-02.bpo-44975.ub3Vxk.rst @@ -0,0 +1,2 @@ +Runtime protocols with data members now support :func:`issubclass` as long +as those members are annotated with :data:`typing.ClassVar`. From f045d2ee1ee09742af9800797b776408bef47f29 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Mon, 23 Aug 2021 17:44:56 +0800 Subject: [PATCH 2/8] make it work with string annotations and PEP 563 --- Lib/test/test_typing.py | 23 +++++++++++++++++++++++ Lib/typing.py | 22 ++++++++++++++++------ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 60c43929f1f0ac..8855db361bd6da 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1622,6 +1622,29 @@ class D: self.assertNotIsSubclass(C, P) self.assertIsSubclass(D, P) + # String / PEP 563 annotations. + @runtime_checkable + class P(Protocol): + x: "ClassVar[int]" = 1 + y: "typing.ClassVar[int]" = 2 + z: "typing_extensions.ClassVar[int]" = 3 + + class D: + x = 1 + y = 2 + z = 3 + self.assertNotIsSubclass(C, P) + self.assertIsSubclass(D, P) + + # Make sure mixed are forbidden. + @runtime_checkable + class P(Protocol): + x: "ClassVar[int]" = 1 + y = 2 + + self.assertRaises(TypeError, issubclass, C, P) + self.assertRaises(TypeError, issubclass, D, P) + class GenericTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index 6c085b6e6cb9c1..8de0d0c7fd0f27 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1395,15 +1395,25 @@ def _get_protocol_attrs(cls): return attrs -def _is_callable_members_only(cls): +def _is_callable_or_classvar_members_only(cls): attr_names = _get_protocol_attrs(cls) annotations = getattr(cls, '__annotations__', {}) # PEP 544 prohibits using issubclass() with protocols that have non-method members. for attr_name in attr_names: attr = getattr(cls, attr_name, None) - if not (callable(attr) - or (getattr(annotations.get(attr_name), '__name__', None) == 'ClassVar')): - return False + if callable(attr): + continue + annotation = annotations.get(attr_name) + if getattr(annotation, '__name__', None) == 'ClassVar': + continue + # String / PEP 563 annotations + # Note: If PEP 649 is accepted, we can probably drop this. + if isinstance(annotation, str): + if (annotation.startswith('ClassVar[') + or annotation.startswith('typing.ClassVar[') + or annotation.startswith('typing_extensions.ClassVar[')): + continue + return False return True @@ -1450,7 +1460,7 @@ def __instancecheck__(cls, instance): " @runtime_checkable protocols") if ((not getattr(cls, '_is_protocol', False) or - _is_callable_members_only(cls)) and + _is_callable_or_classvar_members_only(cls)) and issubclass(instance.__class__, cls)): return True if cls._is_protocol: @@ -1515,7 +1525,7 @@ def _proto_hook(other): return NotImplemented raise TypeError("Instance and class checks can only be used with" " @runtime_checkable protocols") - if not _is_callable_members_only(cls): + if not _is_callable_or_classvar_members_only(cls): if _allow_reckless_class_checks(): return NotImplemented raise TypeError("Protocol members must be methods or data" From 58c69e1ca7e0e00977f8d43cff37feee87b2b9db Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Tue, 24 Aug 2021 21:14:18 +0800 Subject: [PATCH 3/8] Apply Lukasz' suggestions --- Lib/test/test_typing.py | 2 +- Lib/typing.py | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 8855db361bd6da..293d8a32a65b06 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1622,7 +1622,7 @@ class D: self.assertNotIsSubclass(C, P) self.assertIsSubclass(D, P) - # String / PEP 563 annotations. + # String annotations (forward references). @runtime_checkable class P(Protocol): x: "ClassVar[int]" = 1 diff --git a/Lib/typing.py b/Lib/typing.py index 8de0d0c7fd0f27..5154356b874752 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1394,6 +1394,7 @@ def _get_protocol_attrs(cls): attrs.add(attr) return attrs +_classvar_prefixes = ("typing.ClassVar[", "t.ClassVar[", "ClassVar[") def _is_callable_or_classvar_members_only(cls): attr_names = _get_protocol_attrs(cls) @@ -1406,13 +1407,9 @@ def _is_callable_or_classvar_members_only(cls): annotation = annotations.get(attr_name) if getattr(annotation, '__name__', None) == 'ClassVar': continue - # String / PEP 563 annotations - # Note: If PEP 649 is accepted, we can probably drop this. - if isinstance(annotation, str): - if (annotation.startswith('ClassVar[') - or annotation.startswith('typing.ClassVar[') - or annotation.startswith('typing_extensions.ClassVar[')): - continue + # String annotations (forward references). + if isinstance(annotation, str) and annotation.startswith(_classvar_prefixes): + continue return False return True From 2ed2ee5df19e32420de8e6da19917e3319814483 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 25 Aug 2021 01:10:07 +0800 Subject: [PATCH 4/8] fix test --- Lib/test/test_typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 293d8a32a65b06..dc4ddf7f52fdf0 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1627,7 +1627,7 @@ class D: class P(Protocol): x: "ClassVar[int]" = 1 y: "typing.ClassVar[int]" = 2 - z: "typing_extensions.ClassVar[int]" = 3 + z: "t.ClassVar[int]" = 3 class D: x = 1 From c8423705307ee8a7242850afc7fee698329ddf9a Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Thu, 26 Aug 2021 20:42:10 +0800 Subject: [PATCH 5/8] Use identity check instead of equality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ɓukasz Langa --- Lib/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index 5154356b874752..6bcbd2553f5089 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1405,7 +1405,7 @@ def _is_callable_or_classvar_members_only(cls): if callable(attr): continue annotation = annotations.get(attr_name) - if getattr(annotation, '__name__', None) == 'ClassVar': + if getattr(annotation, '__origin__', None) is ClassVar: continue # String annotations (forward references). if isinstance(annotation, str) and annotation.startswith(_classvar_prefixes): From 9278a346d7e1a474afd1be9d4e5aa40dbb307194 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 5 Dec 2021 22:41:04 +0800 Subject: [PATCH 6/8] fix merge issues --- Doc/whatsnew/3.11.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst index 5182f9cb258958..056d6a34fb6c3b 100644 --- a/Doc/whatsnew/3.11.rst +++ b/Doc/whatsnew/3.11.rst @@ -269,6 +269,21 @@ sqlite3 (Contributed by Aviv Palivoda, Daniel Shahaf, and Erlend E. Aasland in :issue:`16379` and :issue:`24139`.) +* Add :meth:`~sqlite3.Connection.setlimit` and + :meth:`~sqlite3.Connection.getlimit` to :class:`sqlite3.Connection` for + setting and getting SQLite limits by connection basis. + (Contributed by Erlend E. Aasland in :issue:`45243`.) + +* :mod:`sqlite3` now sets :attr:`sqlite3.threadsafety` based on the default + threading mode the underlying SQLite library has been compiled with. + (Contributed by Erlend E. Aasland in :issue:`45613`.) + +* :mod:`sqlite3` C callbacks now use unraisable exceptions if callback + tracebacks are enabled. Users can now register an + :func:`unraisable hook handler ` to improve their debug + experience. + (Contributed by Erlend E. Aasland in :issue:`45828`.) + typing ------ @@ -403,7 +418,6 @@ Deprecated as deprecated, its docstring is now corrected). (Contributed by Hugo van Kemenade in :issue:`45837`.) - Removed ======= From ba57bb1c2da82fcce41f029205455f5f20d34739 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 5 Dec 2021 23:31:54 +0800 Subject: [PATCH 7/8] Add more tests and comments --- Lib/test/test_typing.py | 6 ++++++ Lib/typing.py | 13 +++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 6b59d34bea355c..20778effa85b30 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1631,8 +1631,13 @@ class C: pass class D: x = 1 + + class E: + x = 2 + self.assertNotIsSubclass(C, P) self.assertIsSubclass(D, P) + self.assertIsSubclass(E, P) # String annotations (forward references). @runtime_checkable @@ -1645,6 +1650,7 @@ class D: x = 1 y = 2 z = 3 + self.assertNotIsSubclass(C, P) self.assertIsSubclass(D, P) diff --git a/Lib/typing.py b/Lib/typing.py index 354e49cc12913d..aca14efb6fe1bc 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1404,21 +1404,26 @@ def _get_protocol_attrs(cls): attrs.add(attr) return attrs -_classvar_prefixes = ("typing.ClassVar[", "t.ClassVar[", "ClassVar[") +_CLASSVAR_PREFIXES = ("typing.ClassVar", "t.ClassVar", "ClassVar") -def _is_callable_or_classvar_members_only(cls): +def _is_callable_or_classvar_members_only(cls, instance=None, verify_classvar_values=False): attr_names = _get_protocol_attrs(cls) annotations = getattr(cls, '__annotations__', {}) # PEP 544 prohibits using issubclass() with protocols that have non-method members. + # bpo-44975: Relaxing that restriction to allow for runtime-checkable + # protocols with class variables since those should be available at class + # definition time. for attr_name in attr_names: attr = getattr(cls, attr_name, None) + # Method-like. if callable(attr): continue annotation = annotations.get(attr_name) + # ClassVar member if getattr(annotation, '__origin__', None) is ClassVar: continue - # String annotations (forward references). - if isinstance(annotation, str) and annotation.startswith(_classvar_prefixes): + # ClassVar string annotations (forward references). + if isinstance(annotation, str) and annotation.startswith(_CLASSVAR_PREFIXES): continue return False return True From 43ec2ea8e4e96723da482440318e6644b69a3006 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Mon, 6 Dec 2021 00:22:29 +0800 Subject: [PATCH 8/8] support value checking classvars --- Lib/test/test_typing.py | 8 ++++++++ Lib/typing.py | 30 ++++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 20778effa85b30..703f5a28c4a2c0 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -1635,18 +1635,26 @@ class D: class E: x = 2 + class F: + x = 'foo' + self.assertNotIsSubclass(C, P) self.assertIsSubclass(D, P) self.assertIsSubclass(E, P) + self.assertNotIsSubclass(F, P) # String annotations (forward references). @runtime_checkable class P(Protocol): + # Special case, bare ClassVar, our checks should + # just skip these. + w: "ClassVar" x: "ClassVar[int]" = 1 y: "typing.ClassVar[int]" = 2 z: "t.ClassVar[int]" = 3 class D: + w = 0 x = 1 y = 2 z = 3 diff --git a/Lib/typing.py b/Lib/typing.py index aca14efb6fe1bc..e6b0de32f37e5e 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1406,7 +1406,10 @@ def _get_protocol_attrs(cls): _CLASSVAR_PREFIXES = ("typing.ClassVar", "t.ClassVar", "ClassVar") -def _is_callable_or_classvar_members_only(cls, instance=None, verify_classvar_values=False): +def _is_callable_or_classvar_members_only(cls, instance): + """Returns a 2-tuple signalling two things: + (Valid protocol?, If not valid protocol, was it due to ClassVar value mismatch?) + """ attr_names = _get_protocol_attrs(cls) annotations = getattr(cls, '__annotations__', {}) # PEP 544 prohibits using issubclass() with protocols that have non-method members. @@ -1421,12 +1424,23 @@ def _is_callable_or_classvar_members_only(cls, instance=None, verify_classvar_va annotation = annotations.get(attr_name) # ClassVar member if getattr(annotation, '__origin__', None) is ClassVar: + instance_attr = getattr(instance, attr_name, None) + # If we couldn't find anything, don't bother checking value types. + if (instance_attr is not None + and attr is not None + and type(instance_attr) != type(attr)): + return False, True continue # ClassVar string annotations (forward references). if isinstance(annotation, str) and annotation.startswith(_CLASSVAR_PREFIXES): + instance_attr = getattr(instance, attr_name, None) + if (instance_attr is not None + and attr is not None + and type(instance_attr) != type(attr)): + return False, True continue - return False - return True + return False, False + return True, False def _no_init_or_replace_init(self, *args, **kwargs): @@ -1496,10 +1510,9 @@ def __instancecheck__(cls, instance): ): raise TypeError("Instance and class checks can only be used with" " @runtime_checkable protocols") - if ((not getattr(cls, '_is_protocol', False) or - _is_callable_or_classvar_members_only(cls)) and - issubclass(instance.__class__, cls)): + _is_callable_or_classvar_members_only(cls, instance)[0]) and + issubclass(instance.__class__, cls)): return True if cls._is_protocol: if all(hasattr(instance, attr) and @@ -1563,8 +1576,9 @@ def _proto_hook(other): return NotImplemented raise TypeError("Instance and class checks can only be used with" " @runtime_checkable protocols") - if not _is_callable_or_classvar_members_only(cls): - if _allow_reckless_class_checks(): + ok_members, classvar_mismatch = _is_callable_or_classvar_members_only(cls, other) + if not ok_members: + if _allow_reckless_class_checks() or classvar_mismatch: return NotImplemented raise TypeError("Protocol members must be methods or data" " attributes annotated with ClassVar to support"