Add PEP 8-compatible attribute aliases to an object

# -*- coding: utf-8 -*-

"""
PEP 8 Attributes
~~~~~~~~~~~~~~~~

This adds :PEP:`8` compatible aliases for object attributes which are not.

:Copyright: 2007 Jochen Kupperschmidt
:Date: 11-Jul-2007
:License: MIT

.. _PEP 8:  http://www.python.org/dev/peps/pep-0008/
"""

from itertools import ifilterfalse
from warnings import warn


def add_aliases(obj):
    """Add aliases to the object.

    Attributes beginning with an underscore are always skipped since they
    should not be meant for external use.

    A warning is issued if the new attribute already exists.
    """
    attrs = dir(obj)

    # Skip internal attributes.
    def startswith(s):
        return s.startswith('_')
    attrs = ifilterfalse(startswith, attrs)

    # Skip attributes compatible to PEP 8.
    attrs = ifilterfalse(str.islower, attrs)

    # Add attribute aliases.
    for attr in attrs:
        pep8_attr = str(AttributeName(attr))
        if hasattr(obj, pep8_attr):
            warn("Attribute '%s' exists, not replacing." % pep8_attr)
        else:
            setattr(obj, pep8_attr, getattr(obj, attr))


class AttributeName(object):
    """A name for a function, method or instance variable that obeys the
    naming style suggested by :PEP:`8`.
    """

    def __init__(self, name):
        self.words = []
        self.current_word = []
        last_upper = False
        uppercase_word = False

        for char in name:
            isupper = char.isupper()
            if isupper:
                if not last_upper:
                    self.push_word()
                uppercase_word = last_upper
                char = char.lower()
            elif char == '_':
                # An underscore indicates a word boundary.
                self.push_word()
                continue
            elif uppercase_word and last_upper:
                # An all-uppercase word was detected.  The previous character
                # already belongs to the next word, so transfer it.
                prev_char = self.current_word.pop()
                self.push_word()
                self.current_word.append(prev_char)
            last_upper = isupper
            self.current_word.append(char)

        self.push_word()

    def push_word(self):
        """Push the current word into the words list."""
        if self.current_word:
            self.words.append(''.join(self.current_word))
            self.current_word = []

    def __str__(self):
        return '_'.join(self.words)


# Tests to be executed with py.test_.
#
# .. _py.test: http://codespeak.net/py/dist/test.html

def test_add_aliases():
    class X(object):
        pass
    x = X()
    for attr in ('fooBar', 'FooBar'):
        setattr(x, attr, None)

    attr_len = len(dir(x))
    # TODO: Assert warning?
    add_aliases(x)
    assert attr_len + 1 == len(dir(x))

def test_AttributeName():
    translations = (
        ('lower', 'lower'),
        ('lower_with_underscores', 'lower_with_underscores'),
        ('Capitalized', 'capitalized'),
        ('CamelCase', 'camel_case'),
        ('mixedCase', 'mixed_case'),
        ('CamelCaseWithAllCAPS', 'camel_case_with_all_caps'),
        ('AllCAPSContainedSomething', 'all_caps_contained_something'),
        ('Capitalized_Words_With_Underscores',
            'capitalized_words_with_underscores'),
        )
    for found_input, expected_output in translations:
        assert str(AttributeName(found_input)) == expected_output