Repository: pydantic/pydantic

Issue:
@validate_call on instance method of class with `__slots__` raises "AttributeError: object attribute is read-only"
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

When the @validate_call decorator is applied to an instance method of a class with `__slots__` set, calls to the instance method raise an AttributeError:
```
Traceback (most recent call last):
  File "./test_validate_call.py", line 25, in <module>
    c.any_instance_method()
  File "./.venv/lib/python3.8/site-packages/pydantic/_internal/_validate_call.py", line 118, in __get__
    object.__setattr__(obj, self._name, result)
AttributeError: 'ClassWithSlots' object attribute 'any_instance_method' is read-only
```

This error is not raised when using the `@validate_arguments` decorator instead of `@validate_call`.

### Example Code

```Python
from pydantic import validate_call

class ClassWithSlots():
    __slots__ = {}

    @validate_call
    def any_instance_method(self):
        pass

c = ClassWithSlots()
c.any_instance_method()
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.3.0
        pydantic-core version: 2.6.3
          pydantic-core build: profile=release pgo=true
                 install path: ./.venv/lib/python3.8/site-packages/pydantic
               python version: 3.8.10 (default, May 26 2023, 14:05:08)  [GCC 9.4.0]
                     platform: Linux-5.15.0-83-generic-x86_64-with-glibc2.29
     optional deps. installed: ['typing-extensions']
```



Discussion:
Hi @PeterL4,

Thanks for your question! What's happening here is that when you use the `@validate_call` decorator on your method, internally, Pydantic is attempting to wrap that function in a `ValidateCallWrapper` and call `setattr` on the the object to reassign the function name to be associated with the new, wrapped function.

When you define `__slots__` as an empty set, that prevents access to `__dict__`, which is required for assigning instance vars on object instances (which is what Pydantic is trying to do).

One quick workaround is the following:
```py
from pydantic import validate_call


class ClassWithSlots:
    __slots__ = {'__dict__'}

    @validate_call
    def any_instance_method(self):
        pass

c = ClassWithSlots()
c.any_instance_method()
```

Let me know if this works for your use case. If not, we can get more creative 😄.
The history of why we introduced `setattr` is in the PRs https://github.com/pydantic/pydantic/pull/6406 and https://github.com/pydantic/pydantic/pull/6431

The motivation was for equality of decorated methods as well as performance. Ideally we can rework to keep these properties but avoid needing to mutate the object.

Resolving patch:
diff --git a/pydantic/_internal/_validate_call.py b/pydantic/_internal/_validate_call.py
--- a/pydantic/_internal/_validate_call.py
+++ b/pydantic/_internal/_validate_call.py
@@ -108,6 +108,11 @@ def __get__(self, obj: Any, objtype: type[Any] | None = None) -> ValidateCallWra
 
         bound_function = self.raw_function.__get__(obj, objtype)
         result = self.__class__(bound_function, self._config, self._validate_return)
+
+        # skip binding to instance when obj or objtype has __slots__ attribute
+        if hasattr(obj, '__slots__') or hasattr(objtype, '__slots__'):
+            return result
+
         if self._name is not None:
             if obj is not None:
                 object.__setattr__(obj, self._name, result)
@@ -120,3 +125,6 @@ def __set_name__(self, owner: Any, name: str) -> None:
 
     def __repr__(self) -> str:
         return f'ValidateCallWrapper({self.raw_function})'
+
+    def __eq__(self, other):
+        return self.raw_function == other.raw_function


Test patch:
diff --git a/tests/test_validate_call.py b/tests/test_validate_call.py
--- a/tests/test_validate_call.py
+++ b/tests/test_validate_call.py
@@ -725,3 +725,32 @@ async def foo(a: Any) -> int:
             'input': 'x',
         }
     ]
+
+
+def test_validate_call_with_slots() -> None:
+    class ClassWithSlots:
+        __slots__ = {}
+
+        @validate_call(validate_return=True)
+        def some_instance_method(self, x: str) -> str:
+            return x
+
+        @classmethod
+        @validate_call(validate_return=True)
+        def some_class_method(cls, x: str) -> str:
+            return x
+
+        @staticmethod
+        @validate_call(validate_return=True)
+        def some_static_method(x: str) -> str:
+            return x
+
+    c = ClassWithSlots()
+    assert c.some_instance_method(x='potato') == 'potato'
+    assert c.some_class_method(x='pepper') == 'pepper'
+    assert c.some_static_method(x='onion') == 'onion'
+
+    # verify that equality still holds for instance methods
+    assert c.some_instance_method == c.some_instance_method
+    assert c.some_class_method == c.some_class_method
+    assert c.some_static_method == c.some_static_method