Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python String Templating with Case Sensitivity

This is a naming script that i use to name nodes in Autodesk Maya. This particular script however doesnt use anything maya specific.

I had asked a while ago how I would go about doing something like this, where a variable convention could be used, and templating came up.

So if i had a convention like this:

'${prefix}_${name}_${side}_${type}'

I could pass these arguments:

bind_thigh_left_joint

And then run them through an abbreviation dictionary (as well as a user abbreviation dictionary), check it with relevant nodes in scene file to make sure there are no duplicates, and end up with this: bn_thigh_L_jnt

However I wanted it so that if one of the keys has a first uppercase letter, it would make the substitute uppercase.

For example if {$prefix} was instead {$Prefix} thigh would become Thigh, or if {$prefix} was {$PREFIX} thigh would become THIGH. However if it was {$PREfix} thigh would still only be Thigh.

I can do this easily enough except that I have no way to detect the individual cases of the keys. For example, if the string is '${Prefix}_${name}_${SIDE}_${type}' How would I find what case prefix is, and if i knew that, how would i use that with this template?

Note this code isnt the exact code i have, I have ommitted a lot of other stuff that was more maya specific, this just deals with the substituting itself.

from string import Template
import collections

def convert(prefix, name, side, obj_type):
    user_conv = '${Prefix}_${name}_${SIDE}_${type}'
    # Assigns keys to strings to be used by the user dictionary.
    subs = {'prefix': prefix, 'name': name, 'side': side, 'type': obj_type}

    # Converts all of user convention to lowercase, and substitutes the names from subs.
    new_name = Template(user_conv.lower())
    new_name = new_name.safe_substitute(**subs)
    # Strips leading and trailing underscores, and replaces double underscores with a single
    new_name = new_name.strip('_')
    new_name = new_name.replace('__', '_')

    return new_name

print convert('bind', 'thigh', 'left', 'joint')
>> bind_thigh_left_joint

Edit: Also would like to strip multiple underscores

So if I had something like:

'${prefix}___${name}__${side}_____${type}'

I would want it to come out

>> bind_thigh_left_joint

not

>> bind___thigh__left____joint

Also the last thing, I figured since a user would be inputting this, it would be more convenient not to be adding brackets and dollar signs. Would it be possible to do something like this?

import re
user_conv = 'PREFIX_name_Side_TYPE01'
# do all filtering, removing of underscores and special characters
templates = ['prefix', 'name', 'side', 'type']
for template in templates:
    if template in user_conv.lower():
        # add bracket and dollar sign around match

>> '${PREFIX}_{name}_{Side}_${TYPE}01'
like image 328
Neo Conker Avatar asked Mar 04 '15 09:03

Neo Conker


2 Answers

Here, we can use the power of OOP to make the template do what we want to. We can go ahead and extend the string.Template class (as suggested in the docs).

Let us first import some relevant methods/classes:

from string import Template, uppercase, _multimap
import collections

We then define a helper method for dealing with arguments passed to the safe_substitute() or substitute() method. (The meat for this method was taken from Python's string module source):

def get_mapping_from_args(*args, **kws):
    if len(args) > 1:
        raise TypeError('Too many positional arguments')
    if not args:
        mapping = kws
    elif kws:
        mapping = _multimap(kws, args[0])
    else:
        mapping = args[0]             
    return mapping

We then go ahead and define our extended Template class. Let us call this class CustomRenameTemplate. We write a helper method called do_template_based_capitalization(), that basically does the capitalization based on the template pattern you have provided. We make sure we override the substitute() and safe_substitute() methods to use this.

class CustomRenameTemplate(Template):    
    def __init__(self, *args, **kws):        
        super(CustomRenameTemplate, self).__init__(*args, **kws)
        self.orig_template = self.template
        self.template = self.template.lower()    

    def do_template_based_capitalization(self, mapping):
        matches = self.pattern.findall(self.orig_template)
        for match in matches:
            keyword = match[self.pattern.groupindex['braced']-1]
            if keyword[0] in uppercase:  # First letter is CAPITALIZED
                if keyword == keyword.upper():  # Condition for full capitalization
                    mapping[keyword.lower()] = mapping[keyword.lower()].upper()
                else:  # Condition for only first letter capitalization
                    mapping[keyword.lower()] = mapping[keyword.lower()].capitalize()   

    def safe_substitute(self, *args, **kws):
        mapping = get_mapping_from_args(*args, **kws)
        self.do_template_based_capitalization(mapping)
        return super(CustomRenameTemplate, self).safe_substitute(mapping)

    def substitute(self, *args, **kws):
        mapping = get_mapping_from_args(*args, **kws)
        self.do_template_based_capitalization(mapping)
        return super(CustomRenameTemplate, self).substitute(mapping)

We are ready to use this class now. We go ahead and do some slight modifications to your convert() method to put this new class into action:

def convert(prefix, name, side, obj_type, user_conv='${Prefix}_${name}_${SIDE}_${type}'):
    # Let us parameterize user_conv instead of hardcoding it.
    # That makes for better testing, modularity and all that good stuff.
    # user_conv = '${Prefix}_${name}_${SIDE}_${type}'
    # Assigns keys to strings to be used by the user dictionary.
    subs = {'prefix': prefix, 'name': name, 'side': side, 'type': obj_type}

    # Converts all of user convention to lowercase, and substitutes the names from subs.
    new_name = CustomRenameTemplate(user_conv)  # Send the actual template, instead of it's lower()
    new_name = new_name.substitute(**subs)

    # Strips leading and trailing underscores, and replaces double underscores with a single
    new_name = new_name.strip('_')
    new_name = new_name.replace('__', '_')

    return new_name

And here's it in action:

>>>print convert('bind', 'thigh', 'left', 'joint')
Bind_thigh_LEFT_joint

>>>print convert('bind', 'thigh', 'left', 'joint', user_conv='${prefix}_${name}_${side}_${type}')
bind_thigh_left_joint

>>>print convert('bind', 'thigh', 'left', 'joint', user_conv='${prefix}_${NAme}_${side}_${TYPE}')
bind_Thigh_left_JOINT

Update #1:

If you want to deal with multiple occurrences of the underscore _ and possible special characters in the user convention, just add the following lines before the return statement of the convert() method:

new_name = re.sub('[^A-Za-z0-9_]+', '', new_name)  # This will strip every character NOT ( NOT is denoted by the leading ^) enclosed in the []
new_name = re.sub('_+', '_', new_name)  # This will replace one or more occurrences of _ with a single _

Note: An important thing to consider when stripping away special characters is the special characters used by Maya egs. for namespace representation : and for hierarchy representation |. I will leave it up to you to either choose to strip these away, or replace them with another character, or to not receive them in the first place. Most Maya commands that return an object name(s) have flags to control the verbosity of the name returned (i.e. egs. WITH namespace, full DAG path, or none of these).

Update #2:

For the extended portion of your question, where you had asked:

Also the last thing, I figured since a user would be inputting this, it would be more convenient not to be adding brackets and dollar signs. Would it be possible to do something like this?

Yes. In fact, to generalize that further, if you assume that the template strings will only by alpha and not alpha-numeric, you can again use re to pick them up from the user_conv and stuff them inside ${} like so:

user_conv = 'PREFIX_name_Side_TYPE01'
user_conv = re.sub('[A-Za-z]+', '${\g<0>}', user_conv)

>>> print user_conv
>>> ${PREFIX}_${name}_${Side}_${TYPE}01

We used the power of backreferences here i.e. with \g<group_number>. Check the docs here for more information on backreferences in regular expressions.

like image 104
kartikg3 Avatar answered Oct 05 '22 14:10

kartikg3


Create duplicate substitutions for each capitalization you want to support. Loop over the key/value pairs in your original subs dictionary with dict.items() or dict.iteritems(). 'KEY': 'VALUE' and 'Key': 'Value' pairs are easy to create with .upper() and .title().

If supporting 'KEy': 'Value' is really important to you, that can be done by looping over indices of the key, splitting, upper-casing the first part, and re-combining. For example, if key is 'Hello',

key[:2].upper() + key[2:]

will be 'HEllo'.

Then, just use safe_substitute as normal.

like image 35
Dan Getz Avatar answered Oct 05 '22 16:10

Dan Getz