From e32cde9f9c5c1e3e098e51c1e0a7e023830bb4d5 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sun, 5 Mar 2023 18:29:55 +0000 Subject: [PATCH 1/6] gh-102433: Add tests for how properties interact with `typing.runtime_checkable` protocols --- Lib/test/test_typing.py | 122 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 2eeaf91d78d8f3..a873c7eff9f1f6 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2535,6 +2535,128 @@ def meth(x): ... with self.assertRaises(TypeError): isinstance(C(), BadPG) + def test_protocols_isinstance_simple_properties(self): + T = TypeVar('T') + + @runtime_checkable + class P(Protocol): + @property + def attr(self): ... + + @runtime_checkable + class P1(Protocol): + attr: int + + @runtime_checkable + class PG(Protocol[T]): + @property + def attr(self): ... + + @runtime_checkable + class PG1(Protocol[T]): + attr: T + + class BadP(Protocol): + @property + def attr(self): ... + + class BadP1(Protocol): + attr: int + + class BadPG(Protocol[T]): + @property + def attr(self): ... + + class BadPG1(Protocol[T]): + attr: T + + class C: + @property + def attr(self): + return 42 + + self.assertEqual(C().attr, 42) + self.assertIsInstance(C(), P) + self.assertIsInstance(C(), P1) + self.assertIsInstance(C(), PG) + self.assertIsInstance(C(), PG1) + with self.assertRaises(TypeError): + isinstance(C(), PG[T]) + with self.assertRaises(TypeError): + isinstance(C(), PG[C]) + with self.assertRaises(TypeError): + isinstance(C(), PG1[T]) + with self.assertRaises(TypeError): + isinstance(C(), PG1[C]) + with self.assertRaises(TypeError): + isinstance(C(), BadP) + with self.assertRaises(TypeError): + isinstance(C(), BadP1) + with self.assertRaises(TypeError): + isinstance(C(), BadPG) + with self.assertRaises(TypeError): + isinstance(C(), BadPG1) + + def test_protocols_isinstance_dynamic_properties(self): + T = TypeVar('T') + + @runtime_checkable + class P(Protocol): + @property + def attr(self): ... + + @runtime_checkable + class P1(Protocol): + attr: int + + @runtime_checkable + class PG(Protocol[T]): + @property + def attr(self): ... + + @runtime_checkable + class PG1(Protocol[T]): + attr: T + + class C: + X = True + @property + def attr(self): + if self.X: + return 42 + raise AttributeError + + inst = C() + C.X = False + + with self.assertRaises(AttributeError): + C().attr + self.assertNotIsInstance(C(), P) + self.assertNotIsInstance(C(), P1) + self.assertNotIsInstance(C(), PG) + self.assertNotIsInstance(C(), PG1) + + with self.assertRaises(AttributeError): + inst.attr + self.assertNotIsInstance(inst, P) + self.assertNotIsInstance(inst, P1) + self.assertNotIsInstance(inst, PG) + self.assertNotIsInstance(inst, PG1) + + C.X = True + + self.assertEqual(C().attr, 42) + self.assertIsInstance(C(), P) + self.assertIsInstance(C(), P1) + self.assertIsInstance(C(), PG) + self.assertIsInstance(C(), PG1) + + self.assertEqual(inst.attr, 42) + self.assertIsInstance(inst, P) + self.assertIsInstance(inst, P1) + self.assertIsInstance(inst, PG) + self.assertIsInstance(inst, PG1) + def test_protocols_isinstance_py36(self): class APoint: def __init__(self, x, y, label): From 74eb79545a32c8b31a843c721107be87a9c309df Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 10 Mar 2023 17:06:09 +0000 Subject: [PATCH 2/6] Don't assert undesirable behaviour; cover other situations too --- Lib/test/test_typing.py | 133 +++++++++++++++------------------------- 1 file changed, 51 insertions(+), 82 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index a873c7eff9f1f6..a821e05bfa7fa4 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2535,7 +2535,29 @@ def meth(x): ... with self.assertRaises(TypeError): isinstance(C(), BadPG) - def test_protocols_isinstance_simple_properties(self): + def test_protocols_isinstance_properties_and_descriptors(self): + class C: + @property + def attr(self): + return 42 + + class CustomDescriptor: + def __get__(self, obj, objtype=None): + return 42 + + class D: + attr = CustomDescriptor() + + class E: ... + + # Check that properties set on superclasses + # are still found by the isinstance() logic + class F(C): ... + class G(D): ... + + self.assertEqual(C().attr, 42) + self.assertEqual(D().attr, 42) + T = TypeVar('T') @runtime_checkable @@ -2556,6 +2578,17 @@ def attr(self): ... class PG1(Protocol[T]): attr: T + for protocol_class in P, P1, PG, PG1: + for klass in C, D, F, G: + with self.subTest( + klass=klass.__name__, + protocol_class=protocol_class.__name__ + ): + self.assertIsInstance(klass(), protocol_class) + + with self.subTest(protocol_class=protocol_class.__name__): + self.assertNotIsInstance(E(), protocol_class) + class BadP(Protocol): @property def attr(self): ... @@ -2570,92 +2603,28 @@ def attr(self): ... class BadPG1(Protocol[T]): attr: T - class C: - @property - def attr(self): - return 42 - - self.assertEqual(C().attr, 42) - self.assertIsInstance(C(), P) - self.assertIsInstance(C(), P1) - self.assertIsInstance(C(), PG) - self.assertIsInstance(C(), PG1) - with self.assertRaises(TypeError): - isinstance(C(), PG[T]) - with self.assertRaises(TypeError): - isinstance(C(), PG[C]) - with self.assertRaises(TypeError): - isinstance(C(), PG1[T]) - with self.assertRaises(TypeError): - isinstance(C(), PG1[C]) - with self.assertRaises(TypeError): - isinstance(C(), BadP) - with self.assertRaises(TypeError): - isinstance(C(), BadP1) - with self.assertRaises(TypeError): - isinstance(C(), BadPG) - with self.assertRaises(TypeError): - isinstance(C(), BadPG1) - - def test_protocols_isinstance_dynamic_properties(self): - T = TypeVar('T') - - @runtime_checkable - class P(Protocol): - @property - def attr(self): ... - - @runtime_checkable - class P1(Protocol): - attr: int - - @runtime_checkable - class PG(Protocol[T]): - @property - def attr(self): ... + for obj in PG[T], PG[C], PG1[T], PG1[C], BadP, BadP1, BadPG, BadPG1: + for klass in C, D, E, F, G: + with self.subTest(klass=klass.__name__, obj=obj): + with self.assertRaises(TypeError): + isinstance(klass(), obj) + def test_protocols_isinstance_not_fooled_by_custom_dir(self): @runtime_checkable - class PG1(Protocol[T]): - attr: T - - class C: - X = True - @property - def attr(self): - if self.X: - return 42 - raise AttributeError - - inst = C() - C.X = False - - with self.assertRaises(AttributeError): - C().attr - self.assertNotIsInstance(C(), P) - self.assertNotIsInstance(C(), P1) - self.assertNotIsInstance(C(), PG) - self.assertNotIsInstance(C(), PG1) - - with self.assertRaises(AttributeError): - inst.attr - self.assertNotIsInstance(inst, P) - self.assertNotIsInstance(inst, P1) - self.assertNotIsInstance(inst, PG) - self.assertNotIsInstance(inst, PG1) + class HasX(Protocol): + x: int - C.X = True + class CustomDirWithX: + x = 10 + def __dir__(self): + return [] - self.assertEqual(C().attr, 42) - self.assertIsInstance(C(), P) - self.assertIsInstance(C(), P1) - self.assertIsInstance(C(), PG) - self.assertIsInstance(C(), PG1) + class CustomDirWithoutX: + def __dir__(self): + return ["x"] - self.assertEqual(inst.attr, 42) - self.assertIsInstance(inst, P) - self.assertIsInstance(inst, P1) - self.assertIsInstance(inst, PG) - self.assertIsInstance(inst, PG1) + self.assertIsInstance(CustomDirWithX(), HasX) + self.assertNotIsInstance(CustomDirWithoutX(), HasX) def test_protocols_isinstance_py36(self): class APoint: From b4ed9770153c6612975a26f59e940d6da27dc80c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 10 Mar 2023 17:08:46 +0000 Subject: [PATCH 3/6] Update Lib/test/test_typing.py --- Lib/test/test_typing.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index a821e05bfa7fa4..d0ece5ee22ed51 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2555,8 +2555,9 @@ class E: ... class F(C): ... class G(D): ... - self.assertEqual(C().attr, 42) - self.assertEqual(D().attr, 42) + for klass in C, D, F, G: + with self.subTest(klass=klass.__name__): + self.assertEqual(klass().attr, 42) T = TypeVar('T') From 14268e5aaa744c48322a36134b77fa4eb49db090 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 10 Mar 2023 17:14:57 +0000 Subject: [PATCH 4/6] nit --- Lib/test/test_typing.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index d0ece5ee22ed51..b8bbd5ec8d4203 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2548,17 +2548,19 @@ def __get__(self, obj, objtype=None): class D: attr = CustomDescriptor() - class E: ... - # Check that properties set on superclasses # are still found by the isinstance() logic - class F(C): ... - class G(D): ... + class E(C): ... + class F(D): ... - for klass in C, D, F, G: + for klass in C, D, E, F: with self.subTest(klass=klass.__name__): self.assertEqual(klass().attr, 42) + class G: ... + + self.assertFalse(hasattr(G(), "attr")) + T = TypeVar('T') @runtime_checkable @@ -2580,7 +2582,7 @@ class PG1(Protocol[T]): attr: T for protocol_class in P, P1, PG, PG1: - for klass in C, D, F, G: + for klass in C, D, E, F: with self.subTest( klass=klass.__name__, protocol_class=protocol_class.__name__ @@ -2588,7 +2590,7 @@ class PG1(Protocol[T]): self.assertIsInstance(klass(), protocol_class) with self.subTest(protocol_class=protocol_class.__name__): - self.assertNotIsInstance(E(), protocol_class) + self.assertNotIsInstance(G(), protocol_class) class BadP(Protocol): @property From f2bc5403fbb34871fda6f9d7cd3dcf1de84dffe6 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 10 Mar 2023 23:34:14 +0000 Subject: [PATCH 5/6] Address review --- Lib/test/test_typing.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index b8bbd5ec8d4203..b79c0827bfd18d 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2553,13 +2553,7 @@ class D: class E(C): ... class F(D): ... - for klass in C, D, E, F: - with self.subTest(klass=klass.__name__): - self.assertEqual(klass().attr, 42) - - class G: ... - - self.assertFalse(hasattr(G(), "attr")) + class Empty: ... T = TypeVar('T') @@ -2590,7 +2584,7 @@ class PG1(Protocol[T]): self.assertIsInstance(klass(), protocol_class) with self.subTest(protocol_class=protocol_class.__name__): - self.assertNotIsInstance(G(), protocol_class) + self.assertNotIsInstance(Empty(), protocol_class) class BadP(Protocol): @property @@ -2607,7 +2601,7 @@ class BadPG1(Protocol[T]): attr: T for obj in PG[T], PG[C], PG1[T], PG1[C], BadP, BadP1, BadPG, BadPG1: - for klass in C, D, E, F, G: + for klass in C, D, E, F, Empty: with self.subTest(klass=klass.__name__, obj=obj): with self.assertRaises(TypeError): isinstance(klass(), obj) From ba178f1639871525f6dfe2f282c7a0224b712884 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 11 Mar 2023 00:33:33 +0000 Subject: [PATCH 6/6] Update Lib/test/test_typing.py Co-authored-by: Carl Meyer --- 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 b79c0827bfd18d..ef7c06fd3e08f7 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2583,7 +2583,7 @@ class PG1(Protocol[T]): ): self.assertIsInstance(klass(), protocol_class) - with self.subTest(protocol_class=protocol_class.__name__): + with self.subTest(klass="Empty", protocol_class=protocol_class.__name__): self.assertNotIsInstance(Empty(), protocol_class) class BadP(Protocol):