# 192. Descriptor Chaining

**1. Basic Descriptor**

* A simple descriptor controlling attribute access.

```python
class UpperCaseDescriptor:
    def __get__(self, instance, owner):
        return instance.__dict__.get("_name", "").upper()
    
    def __set__(self, instance, value):
        instance.__dict__["_name"] = value

class Person:
    name = UpperCaseDescriptor()  # Using the descriptor

p = Person()
p.name = "alice"
print(p.name)  # Output: ALICE
```

🔍 **How it works:**

* The descriptor automatically converts `name` to uppercase when accessed.

***

**2. Chaining Two Descriptors**

* Combining two descriptors to control access.

```python
class UpperCaseDescriptor:
    def __get__(self, instance, owner):
        return instance.__dict__.get("_name", "").upper()
    
    def __set__(self, instance, value):
        instance.__dict__["_name"] = value

class LengthLimitDescriptor:
    def __init__(self, limit):
        self.limit = limit

    def __get__(self, instance, owner):
        return instance.__dict__.get("_name", "")

    def __set__(self, instance, value):
        if len(value) > self.limit:
            raise ValueError(f"Name must be at most {self.limit} characters long.")
        instance.__dict__["_name"] = value

class Person:
    name = LengthLimitDescriptor(10)
    name = UpperCaseDescriptor()  # Chaining descriptors

p = Person()
p.name = "short"
print(p.name)  # Output: SHORT
```

🔍 **How it works:**

* The last assigned descriptor (`UpperCaseDescriptor`) takes precedence.

***

**3. Combining Read-Only and Validation Descriptors**

* Using two descriptors: one for validation and one for read-only access.

```python
class ReadOnly:
    def __get__(self, instance, owner):
        return instance.__dict__["_name"]
    
    def __set__(self, instance, value):
        raise AttributeError("Cannot modify this attribute.")

class ValidateLength:
    def __init__(self, limit):
        self.limit = limit

    def __set__(self, instance, value):
        if len(value) > self.limit:
            raise ValueError(f"Name cannot exceed {self.limit} characters.")
        instance.__dict__["_name"] = value

class Person:
    name = ValidateLength(8)
    name = ReadOnly()  # Chaining

p = Person()
p.name = "Max"
print(p.name)  # Output: Max
p.name = "TooLongName"  # Raises ValueError
```

🔍 **How it works:**

* `ValidateLength` first validates the input.
* `ReadOnly` then prevents modification.

***

**4. Logging Access with a Descriptor**

* Logs access before returning the attribute.

```python
class Logger:
    def __get__(self, instance, owner):
        print("Accessing name attribute")
        return instance.__dict__.get("_name", "")

    def __set__(self, instance, value):
        print(f"Setting name to {value}")
        instance.__dict__["_name"] = value

class Person:
    name = Logger()  # Using logging descriptor

p = Person()
p.name = "John"
print(p.name)
```

🔍 **How it works:**

* Logs every read and write operation.

***

**5. Using Property with Descriptors**

* Combining `property()` with a descriptor.

```python
class Capitalize:
    def __get__(self, instance, owner):
        return instance.__dict__["_name"].capitalize()

    def __set__(self, instance, value):
        instance.__dict__["_name"] = value

class Person:
    _name = ""
    
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, value):
        self._name = Capitalize().__get__(self, Person)  # Using the descriptor

p = Person()
p.name = "john"
print(p.name)  # Output: John
```

🔍 **How it works:**

* `property()` is used along with the `Capitalize` descriptor.

***
