5

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'
Neo Conker
  • 169
  • 1
  • 13

2 Answers2

7

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.

kartikg3
  • 2,590
  • 1
  • 16
  • 23
  • Really clean answer. +1 – DrHaze Mar 04 '15 at 13:36
  • Thank you @DrHaze. Your answers are great too – kartikg3 Mar 04 '15 at 13:37
  • I had forgotten to add, how would i deal with say more than two underscores in a row. Lets say the user convention had three or four underscores in between in in a row , how would I replace them with just 1 underscore without having to keep adding cases for it? Also I had taken out the code, but this is actually in a class, and the user convention is passed through the __init__ constructor class. User conv is accessed through self.user_conv instead – Neo Conker Mar 04 '15 at 20:27
  • Also, if i were to put the CustomRenameTemplate class in another module, do i put the def get_mapping_from_args function outside and before the class? – Neo Conker Mar 04 '15 at 21:37
  • Alright, I got it working just fine, it passes all my various tests. Thanks! Now I just have to figure out the best way to handle extra underscores, and I should probably implement a way to filter out special characters as well that aren't a-z or underscores. – Neo Conker Mar 04 '15 at 22:04
  • I will look into what you ve asked for about the underscores when I have access to the computer. Can you update your question with the stuff about underscores too? – kartikg3 Mar 04 '15 at 22:06
  • I have updated the answer with the underscore and special characters stuff. I have also added an extra note to be considered. Hope it helps. – kartikg3 Mar 04 '15 at 23:29
  • Alright thanks! Better than my solution lol. I had a while loop and did a regex that would search for 2 or more reptititions, then override the new_name variable with a string replace of the captured regex group, and would keep going until all repititions were removed. I added one more question in addition to the question about underscores. I'm having a troubling time trying to figure it out. It would be the last thing i need to complete this setup. – Neo Conker Mar 05 '15 at 05:05
  • All this is 3 questions in one! But I ve updated my answer to answer that anyway. – kartikg3 Mar 05 '15 at 15:06
  • Yeah, sorry i just wanted to finish this problem without having to ask more questions XD. Everything passes my tests and expectations now. Thanks a lot! – Neo Conker Mar 06 '15 at 23:53
  • 1
    For anyone wanting to use this nice answer in Python 3: replace ``uppercase`` with ``ascii_uppercase``, and ``_multimap`` with ``_ChainMap`` (from the collections module which is already imported). – cmarqu Sep 10 '17 at 10:40
  • This kind of code is a nightmare for maintenance: it is not a generic case-insensitive string.Template solution that works as any user would expect. – Tom Aug 18 '23 at 11:54
1

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.

Dan Getz
  • 8,774
  • 6
  • 30
  • 64