# person_step08.py
#
# ICS 33 Spring 2026
# Code Example
#
# One change in this version: ImmutableValues determine the names of the
# underlying attributes that store their values, by reacting to the
# __set_name__ method when it's called.



class ImmutableValue:

    # Rather than telling an ImmutableValue explicitly what its underlying
    # attribute name is, we could let it deduce that for us automatically.
    # __set_name__ is called on a descriptor when it's first stored into a
    # class attribute, so that we'll know both the class it's been stored in
    # and the name of the attribute.
    def __set_name__(self, cls, name):
        self._attribute_name = f'_{name}'


    # Nothing else has changed in ImmutableValue.


    def __get__(self, obj, objtype = None):
        if obj is not None:
            return getattr(obj, self._attribute_name)
        else:
            return self


    def __set__(self, obj, value):
        if obj is not None:
            raise AttributeError('Cannot assign to an immutable attribute')


    def __delete__(self, obj):
        if obj is not None:
            raise AttributeError('Cannot delete an immutable attribute')



class Person:

    # We no longer need to specify the field names here, since ImmutableValue
    # will determine them automatically.
    name = ImmutableValue()
    birthdate  = ImmutableValue()


    # Nothing else about our Person class has changed.


    def __init__(self, name, birthdate):
        self._name = name
        self._birthdate = birthdate


    def __eq__(self, other):
        return isinstance(other, Person) and \
               (self.name, self.birthdate) == (other.name, other.birthdate)


    def __hash__(self):
        return ((self.name, self.birthdate))


    def age(self, as_of_date):
        if self.birthdate > as_of_date:
            raise ValueError(f'Person was not born yet on {as_of_date}')

        years_old = as_of_date.year - self.birthdate.year

        if (self.birthdate.month, self.birthdate.day) >= (as_of_date.month, as_of_date.day):
            years_old -= 1

        return years_old
