Use `inspect.getattr_static` in `_ProtocolMeta.__instancecheck__` (#140)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 82ec660..792f25e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,16 @@
   `isinstance()` checks comparing objects to the protocol. See
   ["What's New in Python 3.12"](https://docs.python.org/3.12/whatsnew/3.12.html#typing)
   for more details.
+- `isinstance()` checks against runtime-checkable protocols now use
+  `inspect.getattr_static()` rather than `hasattr()` to lookup whether
+  attributes exist (backporting https://github.com/python/cpython/pull/103034).
+  This means that descriptors and `__getattr__` methods are no longer
+  unexpectedly evaluated during `isinstance()` checks against runtime-checkable
+  protocols. However, it may also mean that some objects which used to be
+  considered instances of a runtime-checkable protocol on older versions of
+  `typing_extensions` may no longer be considered instances of that protocol
+  using the new release, and vice versa. Most users are unlikely to be affected
+  by this change. Patch by Alex Waygood.
 
 # Release 4.5.0 (February 14, 2023)
 
diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py
index 3a483b5..db4cf89 100644
--- a/src/test_typing_extensions.py
+++ b/src/test_typing_extensions.py
@@ -1658,7 +1658,15 @@
         class PG1(Protocol[T]):
             attr: T
 
-        for protocol_class in P, P1, PG, PG1:
+        @runtime_checkable
+        class MethodP(Protocol):
+            def attr(self): ...
+
+        @runtime_checkable
+        class MethodPG(Protocol[T]):
+            def attr(self) -> T: ...
+
+        for protocol_class in P, P1, PG, PG1, MethodP, MethodPG:
             for klass in C, D, E, F:
                 with self.subTest(
                     klass=klass.__name__,
@@ -1683,7 +1691,12 @@
         class BadPG1(Protocol[T]):
             attr: T
 
-        for obj in PG[T], PG[C], PG1[T], PG1[C], BadP, BadP1, BadPG, BadPG1:
+        cases = (
+            PG[T], PG[C], PG1[T], PG1[C], MethodPG[T],
+            MethodPG[C], BadP, BadP1, BadPG, BadPG1
+        )
+
+        for obj in cases:
             for klass in C, D, E, F, Empty:
                 with self.subTest(klass=klass.__name__, obj=obj):
                     with self.assertRaises(TypeError):
@@ -1706,6 +1719,82 @@
         self.assertIsInstance(CustomDirWithX(), HasX)
         self.assertNotIsInstance(CustomDirWithoutX(), HasX)
 
+    def test_protocols_isinstance_attribute_access_with_side_effects(self):
+        class C:
+            @property
+            def attr(self):
+                raise AttributeError('no')
+
+        class CustomDescriptor:
+            def __get__(self, obj, objtype=None):
+                raise RuntimeError("NO")
+
+        class D:
+            attr = CustomDescriptor()
+
+        # Check that properties set on superclasses
+        # are still found by the isinstance() logic
+        class E(C): ...
+        class F(D): ...
+
+        class WhyWouldYouDoThis:
+            def __getattr__(self, name):
+                raise RuntimeError("wut")
+
+        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
+
+        @runtime_checkable
+        class MethodP(Protocol):
+            def attr(self): ...
+
+        @runtime_checkable
+        class MethodPG(Protocol[T]):
+            def attr(self) -> T: ...
+
+        for protocol_class in P, P1, PG, PG1, MethodP, MethodPG:
+            for klass in C, D, E, F:
+                with self.subTest(
+                    klass=klass.__name__,
+                    protocol_class=protocol_class.__name__
+                ):
+                    self.assertIsInstance(klass(), protocol_class)
+
+            with self.subTest(
+                klass="WhyWouldYouDoThis",
+                protocol_class=protocol_class.__name__
+            ):
+                self.assertNotIsInstance(WhyWouldYouDoThis(), protocol_class)
+
+    def test_protocols_isinstance___slots__(self):
+        # As per the consensus in https://github.com/python/typing/issues/1367,
+        # this is desirable behaviour
+        @runtime_checkable
+        class HasX(Protocol):
+            x: int
+
+        class HasNothingButSlots:
+            __slots__ = ("x",)
+
+        self.assertIsInstance(HasNothingButSlots(), HasX)
+
     def test_protocols_isinstance_py36(self):
         class APoint:
             def __init__(self, x, y, label):
diff --git a/src/typing_extensions.py b/src/typing_extensions.py
index c28680c..fc02392 100644
--- a/src/typing_extensions.py
+++ b/src/typing_extensions.py
@@ -524,7 +524,7 @@
             if is_protocol_cls:
                 for attr in cls.__protocol_attrs__:
                     try:
-                        val = getattr(instance, attr)
+                        val = inspect.getattr_static(instance, attr)
                     except AttributeError:
                         break
                     if val is None and callable(getattr(cls, attr, None)):