summaryrefslogtreecommitdiffstats
path: root/Lib/test
diff options
context:
space:
mode:
authorAlex Waygood <Alex.Waygood@Gmail.com>2024-01-05 01:51:17 (GMT)
committerGitHub <noreply@github.com>2024-01-05 01:51:17 (GMT)
commit8435fbfe4e1adb77ef6652bdcbd473b473a08ba3 (patch)
treef80937770b2b1398029ebd1f7efadcb76a287199 /Lib/test
parent6f90399c23783a5b51be194b02d30d7908d6afc2 (diff)
downloadcpython-8435fbfe4e1adb77ef6652bdcbd473b473a08ba3.zip
cpython-8435fbfe4e1adb77ef6652bdcbd473b473a08ba3.tar.gz
cpython-8435fbfe4e1adb77ef6652bdcbd473b473a08ba3.tar.bz2
[3.12] gh-113320: Reduce the number of dangerous `getattr()` calls when constructing protocol classes (#113401) (#113722)
- Only attempt to figure out whether protocol members are "method members" or not if the class is marked as a runtime protocol. This information is irrelevant for non-runtime protocols; we can safely skip the risky introspection for them. - Only do the risky getattr() calls in one place (the runtime_checkable class decorator), rather than in three places (_ProtocolMeta.__init__, _ProtocolMeta.__instancecheck__ and _ProtocolMeta.__subclasscheck__). This reduces the number of locations in typing.py where the risky introspection could go wrong. - For runtime protocols, if determining whether a protocol member is callable or not fails, give a better error message. I think it's reasonable for us to reject runtime protocols that have members which raise strange exceptions when you try to access them. PEP-544 clearly states that all protocol member must be callable for issubclass() calls against the protocol to be valid -- and if a member raises when we try to access it, there's no way for us to figure out whether it's a callable member or not! (cherry-picked from commit ed6ea3ea79)
Diffstat (limited to 'Lib/test')
-rw-r--r--Lib/test/test_typing.py38
1 files changed, 36 insertions, 2 deletions
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index 5681298..05bc6c4 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -3449,8 +3449,8 @@ class ProtocolTests(BaseTestCase):
self.assertNotIn("__protocol_attrs__", vars(NonP))
self.assertNotIn("__protocol_attrs__", vars(NonPR))
- self.assertNotIn("__callable_proto_members_only__", vars(NonP))
- self.assertNotIn("__callable_proto_members_only__", vars(NonPR))
+ self.assertNotIn("__non_callable_proto_members__", vars(NonP))
+ self.assertNotIn("__non_callable_proto_members__", vars(NonPR))
acceptable_extra_attrs = {
'_is_protocol', '_is_runtime_protocol', '__parameters__',
@@ -3995,6 +3995,40 @@ class ProtocolTests(BaseTestCase):
self.assertNotIsInstance(42, ProtocolWithMixedMembers)
+ def test_nonruntime_protocol_interaction_with_evil_classproperty(self):
+ class classproperty:
+ def __get__(self, instance, type):
+ raise RuntimeError("NO")
+
+ class Commentable(Protocol):
+ evil = classproperty()
+
+ # recognised as a protocol attr,
+ # but not actually accessed by the protocol metaclass
+ # (which would raise RuntimeError) for non-runtime protocols.
+ # See gh-113320
+ self.assertEqual(Commentable.__protocol_attrs__, {"evil"})
+
+ def test_runtime_protocol_interaction_with_evil_classproperty(self):
+ class CustomError(Exception): pass
+
+ class classproperty:
+ def __get__(self, instance, type):
+ raise CustomError
+
+ with self.assertRaises(TypeError) as cm:
+ @runtime_checkable
+ class Commentable(Protocol):
+ evil = classproperty()
+
+ exc = cm.exception
+ self.assertEqual(
+ exc.args[0],
+ "Failed to determine whether protocol member 'evil' is a method member"
+ )
+ self.assertIs(type(exc.__cause__), CustomError)
+
+
class GenericTests(BaseTestCase):
def test_basics(self):