# call_counting2.py
#
# ICS 33 Spring 2026
# Code Example
#
# This is a refinement of our original WithCallsCounting decorator, adding
# its ability to decorate the methods of a class, alongside its existing
# ability to decorate functions.


class WithCallsCounted:
    '''
    A decorator that transforms a function or a method into one that keeps track
    of how many times it's been called.  The resulting function or method will
    have an additional count() method that reports that number of calls.
    '''


    # Being a parameterless decorator (i.e., when we use it, we don't pass arguments
    # to it explicitly), the __init__ method will be called with the original
    # function passed as its only argument.  We'll store that function, as well
    # as initialize our count to zero, since the function hasn't been called yet.
    def __init__(self, func):
        self._func = func
        self._count = 0


    # There are now a couple of places where we need to call the original
    # function, though it may now be either the original function or a
    # bound-method version of it.  In both cases, we'll want to pass all
    # of the arguments and keyword arguments along, and we'll want to
    # increment the count, but we'll take a parameter here so we'll know
    # specifically which function we need to be calling.
    def _call_original(self, func, *args, **kwargs):
        self._count += 1
        return func(*args, **kwargs)


    # When the decorated function is called, we'll call it and pass all
    # of the arguments to it unchanged.
    def __call__(self, *args, **kwargs):
        return self._call_original(self._func, *args, **kwargs)


    # When a method has been decorated, it will be obtained via a class attribute.
    # The presence of this __get__ method means that it will be called whenever
    # what's been decorated is a method (i.e., whenever it's a function that's
    # been obtained via a class attribute).
    #
    # In that case, we'll need to determine whether it needs to be transformed
    # into a bound method.  Normally, the original function (self._func) would
    # have had its __get__ method called, which would either return either that
    # same function or a bound method, depending on the situation.
    #
    # We can't just call self._func.__get__(obj, objtype) and call it a day,
    # though, because the original function doesn't know how to update the
    # count.  So, instead, we'll return a function that transforms and calls
    # the original function.  That way, when a decorated method is called,
    # its __get__ method will return something that can take the place of
    # the original method, but still increment its count.
    def __get__(self, obj, objtype):
        def execute(*args, **kwargs):
            original_func = self._func.__get__(obj, objtype)
            return self._call_original(original_func, *args, **kwargs)

        # We also want the decorated method to have the same count() method
        # that our WithCallsCounted decorator has.
        execute.count = self.count
        return execute


    def count(self):
        'Returns the number of times the decorated function or method has been called'
        return self._count
