# person_step07.py
#
# ICS 33 Spring 2026
# Code Example
#
# In this version, we now have two classes, because we've actually got two
# different problems we're solving:
#
# * ImmutableValue, which is a descriptor that provides the ability for any
#   attribute to be immutable (i.e., we can get its value, but neither set it
#   nor delete it).
# * Person, which is what it was before, except it relies on ImmutableValue
#   to handle the immutability of its name and birthdate attributes, so it
#   no longer needs a __getattr__, __setattr__, or __delattr__ method.



class ImmutableValue:

    # When we construct an ImmutableValue, we'll need to tell it the name of
    # the object attribute into which its value is stored.
    def __init__(self, attribute_name):
        self._attribute_name = attribute_name


    # When we get the value associated with an attribute that's defined in the
    # class as an ImmutableValue, we get the value from the object attribute
    # whose name we were given when the ImmutableValue was constructed.
    def __get__(self, obj, objtype = None):
        # The reason we need to check for None here is because __get__ is called
        # when we say something like Person.name, but we only want to look up
        # the corresponding object attribute when there's a corresponding object
        # (i.e., when we've said p.name instead of Person.name).
        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:

    # By storing descriptors in class attributes, when we try to interact with
    # them, their __get__, __set__, or __delete__ methods will be called.  That's
    # where the immutability is enforced, so we don't need to worry about it here.
    name = ImmutableValue('_name')
    birthdate  = ImmutableValue('_birthdate')


    def __init__(self, name, birthdate):
        # Here, we initialize the underlying attributes with protected names.
        # That's no accident; if we tried to say "self.name = name", that would
        # turn into a call to ImmutableValue.__set__, which would raise an
        # exception.
        self._name = name
        self._birthdate = birthdate


    # __eq__ and __hash__ have reverted to a version we had previously, since
    # we no longer have a single tuple storing all values.  That's a small
    # price to pay for a notion of immutability that we can across classes.


    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
