Welcome, guest | Sign In | My Account | Store | Cart

this recipe contains a function, "rename_members", which takes an object, and two strings describing variable naming conventions (e.g. "allcamel", "underscores", etc). it translates attribute names on the given object from the first naming convention to the second (it doesn't delete the original attributes). the function also takes an optional acceptance function, which will be passed each attribute. the default acceptance function is "callable".

Python, 75 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# this is not entirely serious

import re

_patterns = dict((k, re.compile('_*' + v)) for (k, v)
                    in dict(allcamel=r'(?:[A-Z]+[a-z0-9]*)+$',
                            trailingcamel=r'[a-z]+(?:[A-Z0-9]*[a-z0-9]*)+$',
                            underscores=r'(?:[a-z]+_*)+[a-z0-9]+$').iteritems())

_caseTransition = re.compile('([A-Z][a-z]+)')

def translate(name, _from, to):
    leading_underscores = str()
    while name[0] == '_':
        leading_underscores += '_'
        name = name[1:]

    if _from in ('allcamel', 'trailingcamel'):
        words = _caseTransition.split(name)
    else:
        words = name.split('_')

    words = list(w for w in words if w is not None and 0 < len(w))

    camelize = lambda words: ''.join(w[0].upper() + w[1:] for w in words)

    v = dict(smushed=lambda: ''.join(words).lower(),
             allcamel=lambda: camelize(words),
             trailingcamel=lambda: words[0].lower() + camelize(words[1:]),
             underscores=lambda: '_'.join(words).lower())[to]()

    return leading_underscores + v

def rename_members(obj, _from, to, explode_for_non_matches=True,
                   acceptance_function=callable, debug=False):

    assert _from in _patterns
    if to != 'smushed':
        assert to in _patterns

    for name in dir(obj):
        if name.startswith('__') and name.endswith('__'):
            continue
        thing = getattr(obj, name)
        if not acceptance_function(thing):
            continue
        if _patterns[_from].match(name):
            newname = translate(name, _from, to)
            if hasattr(obj, newname):
                raise ValueError('%r already has a %r attribute' % (obj, newname))
            setattr(obj, newname, thing)
            if debug:
                print obj, ':', name, '->', newname
        else:
            if explode_for_non_matches:
                raise ValueError('attribute %r of %r is not well formed %r' % (name, obj, _from))
    return obj

# examples

class Foo:
    def thisIsAMethod(self):
        pass

rename_members(Foo, 'trailingcamel', 'underscores')
assert hasattr(Foo, 'this_is_a_method')

# you could of course pass a module or an instance or anything else

class Foo:
    def this_is_a_method(self):
        pass

rename_members(Foo, 'underscores', 'allcamel')
assert hasattr(Foo, 'ThisIsAMethod')

possible values of the "_from" parameter are: "allcamel", "trailingcamel", "underscores".

possible values of the "to" parameter are "allcamel", "trailingcamel", "underscores", "smushed".

here are some examples of what the function thinks each name corresponds to:

* ThisIsAllCamel
* thisIsTrailingCamel
* this_is_underscores
* thisissmushed

_from obviously cannot have the value "smushed" because it's unclear where the word boundaries are in strings that are written like that.

if explode_for_non_matches is True, then the function will raise a ValueError if it finds an attribute that passes the acceptance function, but whose name does not match the "from" scheme.

fiddle with the regexes if it makes you happy; here are some of the decisions i made:

* an attribute called 'item' (or anything all lower case) is valid for "trailingcamel" and "underscores", but not valid "allcamel"
* an attribute called 'Item' is valid "allcamel" but not valid anything else.

so converting 'Item' to anything else will result in 'item'. converting 'item' to allcamel will result in 'Item'. the camel case modes think that case transitions are word boundaries, so 'ThisIsAnABCOk' will become 'this_is_an_abc_ok' (trailingcamel -> underscores), but bastardized casings like urllib's 'FancyURLopener' will get mistranslated as 'fancy_ur_lopener' (garbage in, garbage out). the only way to resolve something like that is on a case by case basis (pun not intended).

__special__ attributes are ignored, but leading underscores are legal for all modes.

i'm sure there are some bugs, also, but cursory testing didn't reveal anything.

1 comment

Timur Izhbulatov 15 years ago  # | flag

Thanks! Very useful! But I would suggest using list comprehension instead of generating expressions to make in working with Python 2.3.