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:

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)
            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:
                uppercase_word = last_upper
                char = char.lower()
            elif char == '_':
                # An underscore indicates a word boundary.
            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()
            last_upper = isupper


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

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

# Tests to be executed with py.test_.
# .. _py.test:

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

    attr_len = len(dir(x))
    # TODO: Assert warning?
    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'),
    for found_input, expected_output in translations:
        assert str(AttributeName(found_input)) == expected_output