diff options
author | Alex Waygood <Alex.Waygood@Gmail.com> | 2024-01-05 01:51:17 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-05 01:51:17 (GMT) |
commit | 8435fbfe4e1adb77ef6652bdcbd473b473a08ba3 (patch) | |
tree | f80937770b2b1398029ebd1f7efadcb76a287199 /Lib/test | |
parent | 6f90399c23783a5b51be194b02d30d7908d6afc2 (diff) | |
download | cpython-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.py | 38 |
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): |