<commit_before>import abc
from operator import attrgetter


__all__ = ['Importable']


class Importable(object):
    """An :py:mod:`abc` that represents an element that can be imported.

    This :py:mod:`abc` has two abstract methods that need to be overridden by
    subclasses:

    * :py:meth:`__hash__`
    * :py:meth:`__cmp__`

    When the mutable part of an :py:class:`Importable` changes all the
    registered listeners, if any, must be notified. See
    :py:meth:`register_listener` and :py:meth:`register_change`.

    Each :py:class:`Importable` must know how to generate a hash based on its
    own natural keys. They must also define the equality comparison.  Two
    :py:class:`Importable` instances are considered equal only if their natural
    keys are the same.

    The :py:meth:`update` method is used to check for metadata changes between
    two equal :py:class:`Importable` instances.

    To exemplify the default functionality we first need to create a class that
    override all abstract methods:

    >>> from importtools.importables import MockImportable

    If we register a listener, it will be notified when the metadata changes:

    >>> mi = MockImportable(id=1, a=1, b=2)
    >>> ml = []
    >>> listener = lambda x: ml.append(x)
    >>> mi = mi.register_listener(listener)
    >>> mi.register_change()
    >>> ml
    [<MI a 1, b 2>]

    It's up to the :py:class:`Importable` to define what a change is in its
    ``update`` method, the default implementation doesn't trigger a change if
    the values of the attributes returned by :py:meth:`attrs` are the same:

    >>> mi = MockImportable(id=1, a=1, b=2)
    >>> ml = []
    >>> listener = lambda x: ml.append(x)
    >>> mi = mi.register_listener(listener)
    >>> mi.update(MockImportable(id=1, a=1, b=2))
    >>> ml
    []

    In case the values aren't the same, the :py:class:`Importable` is updated
    and :py:meth:`register_change` is called:

    >>> mi = MockImportable(id=1, a=1, b=2)
    >>> ml = []
    >>> listener = lambda x: ml.append(x)
    >>> mi = mi.register_listener(listener)
    >>> mi.update(MockImportable(id=1, a=2, b=3))
    >>> ml
    [<MI a 2, b 3>]

    Special case for single mutable attr:

    >>> mi = MockImportable(id=1, a=1, b=2)
    >>> mi.attrs = lambda: ['a']
    >>> ml = []
    >>> listener = lambda x: ml.append(x)
    >>> mi = mi.register_listener(listener)
    >>> mi.update(MockImportable(id=1, a=2, b=3))
    >>> ml
    [<MI a 2, b 2>]

    """
    __metaclass__ = abc.ABCMeta
    __slots__ = ('listeners', )

    def __init__(self, *args, **kwargs):
        self.listeners = set()
        super(Importable, self).__init__(*args, **kwargs)

    def register_listener(self, listener):
        """Register a callable  to be notified on changes.

        Registering the same listener multiple times shouldn't result in
        multiple notifications to the same listener on a change.

        Return an :py:class:`Importable` instance equivalent to this one that
        has the listener registered. Can return the same instance.

        """
        assert callable(listener)
        self.listeners.add(listener)
        return self

    def register_change(self):
        """Notify the listeners that a change has occurred."""
        for listener in self.listeners:
            listener(self)

    def attrs(self):
        """Return the metadata attributes. Defaults to the empty list."""
        return []<commit_msg>Added test for default update implementation.<commit_after>import abc
from operator import attrgetter


__all__ = ['Importable']


class Importable(object):
    """An :py:mod:`abc` that represents an element that can be imported.

    This :py:mod:`abc` has two abstract methods that need to be overridden by
    subclasses:

    * :py:meth:`__hash__`
    * :py:meth:`__cmp__`

    When the mutable part of an :py:class:`Importable` changes all the
    registered listeners, if any, must be notified. See
    :py:meth:`register_listener` and :py:meth:`register_change`.

    Each :py:class:`Importable` must know how to generate a hash based on its
    own natural keys. They must also define the equality comparison.  Two
    :py:class:`Importable` instances are considered equal only if their natural
    keys are the same.

    The :py:meth:`update` method is used to check for metadata changes between
    two equal :py:class:`Importable` instances.

    To exemplify the default functionality we first need to create a class that
    override all abstract methods:

    >>> from importtools.importables import MockImportable

    If we register a listener, it will be notified when the metadata changes:

    >>> mi = MockImportable(id=1, a=1, b=2)
    >>> ml = []
    >>> listener = lambda x: ml.append(x)
    >>> mi = mi.register_listener(listener)
    >>> mi.register_change()
    >>> ml
    [<MI a 1, b 2>]

    It's up to the :py:class:`Importable` to define what a change is in its
    ``update`` method, the default implementation doesn't trigger a change if
    the values of the attributes returned by :py:meth:`attrs` are the same:

    >>> mi = MockImportable(id=1, a=1, b=2)
    >>> ml = []
    >>> listener = lambda x: ml.append(x)
    >>> mi = mi.register_listener(listener)
    >>> mi.update(MockImportable(id=1, a=1, b=2))
    >>> ml
    []

    In case the values aren't the same, the :py:class:`Importable` is updated
    and :py:meth:`register_change` is called:

    >>> mi = MockImportable(id=1, a=1, b=2)
    >>> ml = []
    >>> listener = lambda x: ml.append(x)
    >>> mi = mi.register_listener(listener)
    >>> mi.update(MockImportable(id=1, a=2, b=3))
    >>> ml
    [<MI a 2, b 3>]

    Special case for single mutable attr:

    >>> mi = MockImportable(id=1, a=1, b=2)
    >>> mi.attrs = lambda: ['a']
    >>> ml = []
    >>> listener = lambda x: ml.append(x)
    >>> mi = mi.register_listener(listener)
    >>> mi.update(MockImportable(id=1, a=2, b=3))
    >>> ml
    [<MI a 2, b 2>]

    The default :py:meth:`attrs` implementation returns an empty list of
    mutable fields and because of that :py:meth:`update` doesn't detect
    any changes:

    >>> class SimpleImportable(Importable):
    ...     def __init__(self, id, a):
    ...         self.id = id
    ...         self.a = a
    ...         super(SimpleImportable, self).__init__()
    ...     def __hash__(self): return self.id
    ...     def __cmp__(self): return cmp(self.id, other.id)
    ...     def __repr__(self): return '<SI a %s>' % self.a

    >>> mi = SimpleImportable(id=1, a=1)
    >>> ml = []
    >>> listener = lambda x: ml.append(x)
    >>> mi = mi.register_listener(listener)
    >>> mi.update(SimpleImportable(id=1, a=2))
    >>> ml
    []

    """
    __metaclass__ = abc.ABCMeta
    __slots__ = ('listeners', )

    def __init__(self, *args, **kwargs):
        self.listeners = set()
        super(Importable, self).__init__(*args, **kwargs)

    def register_listener(self, listener):
        """Register a callable  to be notified on changes.

        Registering the same listener multiple times shouldn't result in
        multiple notifications to the same listener on a change.

        Return an :py:class:`Importable` instance equivalent to this one that
        has the listener registered. Can return the same instance.

        """
        assert callable(listener)
        self.listeners.add(listener)
        return self

    def register_change(self):
        """Notify the listeners that a change has occurred."""
        for listener in self.listeners:
            listener(self)

    def attrs(self):
        """Return the metadata attributes. Defaults to the empty list."""
        return []
{'fraction_non_alphanumeric': 0.12478854912166558, 'fraction_numerical': 0.007547169811320755, 'mean_word_length': 2.943560800410467, 'pattern_counts': {'":': 0, '<': 10, '<?xml version=': 0, '>': 187, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4595068', 'n_tokens_mistral': 2366, 'n_tokens_neox': 2265, 'n_words': 794}