# string_cleaning.py
#
# ICS 33 Spring 2026
# Code Example
#
# This is an abstract base class that describes a single-method protocol for
# cleaning strings of text in some way, along with a few implementations of it.

import abc
import functools
import numbers



class StringCleaner(abc.ABC):
    'A StringCleaner is an object that can clean a string of text in some way'

    @abc.abstractmethod
    def clean(self, text):
        'Clean the given text using the algorithm provided by this cleaner'

        # We're raising an exception here for good reason.  This enforces that
        # derived classes provide an implementation for this method, and also
        # (rightly) causes an exception to be raised even if a derived class
        # uses super().clean(text) to call it.  In other words, by raising a
        # NotImplementedError here, we're making it abundantly clear that
        # implementing this method is entirely and solely the responsibility
        # of derived classes.
        raise NotImplementedError



class NonLetterRemover(StringCleaner):
    'StringCleaner that cleans a string of text by removing non-letters from it'

    def clean(self, text):
        'Cleans the given text by removing non-letters from it'
        return ''.join(ch for ch in text if ch.isalpha())



class TruncatingCleaner(StringCleaner):
    'StringCleaner that cleans a string of text by truncating it to a length limit'

    def __init__(self, length_limit):
        'Initializes the cleaner with the given length limit'
        if not isinstance(length_limit, numbers.Integral):
            raise ValueError(f'length_limit must be an integer, but was {length_limit}')

        self._length_limit = length_limit


    @property
    def length_limit(self):
        'Returns the length limit for this cleaner'
        return self._length_limit


    def clean(self, text):
        'Cleans the given text by truncating it to the given length limit'
        return text[:self.length_limit]



class StringCleanerSequence(StringCleaner):
    '''StringCleaner that cleans a string of text by applying a sequence of
    StringCleaners to it, one at a time.'''


    def __init__(self, cleaners):
        # We'll assume that our parameter was something iterable, but, to be
        # safe, we'll make a new list to store within our object, so that
        # we can maintain its immutability.
        self._cleaners = list(cleaners)

        # Require all of the provided cleaners to implement StringCleaner.
        if any(not isinstance(cleaner, StringCleaner) for cleaner in self._cleaners):
            raise ValueError('All of the cleaners must implement StringCleaner')


    @property
    def cleaners(self):
        'Generates the cleaners associated with this cleaner, one at a time.'

        # We're returning a generator here for the same reason that we used
        # the list() function in the __init__ method: to protect us from
        # code outside of this class modifying the list of cleaners.
        return (cleaner for cleaner in self._cleaners)


    def clean(self, text):
        'Cleans the given text by applying a sequence of cleaners to it'

        # At each step, we're applying one cleaner to the text, the result
        # of which will be passed to the next one.  Once all of the cleaners
        # have been applied, we'll return the result.
        return functools.reduce(
            lambda text, cleaner: cleaner.clean(text),
            self.cleaners, text)



class IncorrectCleaner(StringCleaner):
    'Incorrectly-written StringCleaner, whose objects you will not be able to create'

    # Because this class has no clean() method, as required by the StringCleaner
    # abstract base class, it will not be possible to create an object of this class.
    pass
