# CollectionProperty avoid duplicate names

I have a CollectionProperty like this:

class RenderGroups(PropertyGroup):
image = StringProperty(name="Image", description="The image-texture of this group")

class MHFCMeshProps(PropertyGroup):
render_groups = CollectionProperty(type=RenderGroups,
name="Render Groups")


Now I don't want duplicate names in the PropertyGroup but I stumbled when I tried to shadow the internal name with a StringProperty. Is there any way the following code:

g = obj.testgroup.render_groups.add()
g.name = "Name"
g.name = "Name"


would result in two different keys "Name" and "Name.001" for example?

Here is my idea: Overwrite the attribute setting function of the class. The following code has been created mostly be the OP, based on that idea. I added some minor formatting for better readability:

import bpy
import re
from bpy.props import *

unique_names = ['name']

class RenderGroups(bpy.types.PropertyGroup):
image = StringProperty(
name="Image",
description="The image-texture of this group")

def __setattr__(self, name, value):

def collection_from_element(self):
# this gets the collection that the element is in
path = self.path_from_id()
match = re.match('(.*)$\d*$', path)
parent = self.id_data
try:
coll_path = match.group(1)
except AttributeError:
raise TypeError("Propery not element in a collection.")
else:
return parent.path_resolve(coll_path)

def new_val(stem, nbr):
# simply for formatting
return '{st}.{nbr:03d}'.format(st=stem, nbr=nbr)

# =====================================================

if name not in unique_names:
# don't want to handle
self[name] = value
return
if value == getattr(self, name):
# check for assignement of current value
return

coll = collection_from_element(self)
if value not in coll:
# if value is not in the collection, just assign
self[name] = value
return

# see if value is already in a format like 'name.012'
match = re.match('(.*)\.(\d{3,})', value)
if match is None:
stem, nbr = value, 1
else:
stem, nbr = match.groups()

# check for each value if in collection
new_value = new_val(stem, nbr)
while new_value in coll:
nbr += 1
new_value = new_val(stem, nbr)
self[name] = new_value

class MHFCMeshProps(bpy.types.PropertyGroup):
render_groups = CollectionProperty(
type=RenderGroups,
name="Render Groups")

if __name__ == "__main__":
bpy.utils.register_class(RenderGroups)
bpy.utils.register_class(MHFCMeshProps)
bpy.types.Object.testprop = PointerProperty(type=MHFCMeshProps)


Now you can do this in the blender python console:

obj=bpy.context.object
g.name, g.whatever="testme", "keepme"
g.name, g['whatever']
('testme', 'keepme')

g.name, g.whatever="testme", "keepme"
g.name, g['whatever']
('testme.001', 'keepme')


I think the code example above is now pretty much complete. Although there is still one caveat here. This keeps track of reassigning the current value and always assigns the lowest nbr possible.

• That is a great approach and possibly solves the issue. Will try to think of a solution to the "one caveat" you are talking about because that would feel a bit weird for the user. Sep 18 '14 at 9:00
• I added a quick check for avoiding to reassign the same value twice. A better solution could be to store the list of appended numbers (001, 002, ...) in the value_dict instead of only the counter (id). Then when assigning a new value, the old value's appended number could be removed from the value_dict, then the new value's number can be calculated by iterating the value_dict and search for the first "free" entry. Do you want me to change the example code to show the idea in action ? Sep 18 '14 at 11:42
• Rewritten the script myself based on your idea with __setattr__(). Works fine the way it is, maybe you can find a way to improve or speed it up Sep 18 '14 at 12:36
• i am not sure if this is really working correct. create a testprop and name it g.name="testme", then create another testprops and name it g.name="testme" the second testprop is now named "testme.001" as expected. now rename the second testprop again g.name="testme" now the name is changed to "testme.002" while i expected it to remain "testme.001". BTW i believe there is a bug in one of the regular expressions. Around line 40: Shouldn't it belike this ? match = re.match('(.*)\.(\d{3,})', value) Sep 18 '14 at 13:28
• oops, i fixed the bug as well and made some minor changes to improve readability. Sep 18 '14 at 13:41

There is no way with the api I think. I would suggest to implement your own function:

#collection:    collection, where you want to add a new entry
#name:          the 'original' name, will maybe modified
#check if name allready exists in the given collection
if name in collection:
try:
#check if the name has allready a number
#if not this will produce an error and except will be executed
if name[-4:-3] == '.': #check for a point
int(name[-3:])
#cut out the 'original' name
#([:-4] remove last 4 chars)
name = name[:-4]
except:
#if its not an integer, do nothing
pass

i = 1
#generate three chars with '0' ('{:0>3}') and
#replace it with the next number (.format)
#until a free number is found
while (name + '.{:0>3}'.format(str(i))) in collection:
#otherwise continue counting
i += 1
name = name + '.{:0>3}'.format(str(i))
# ... and assign name
itm.name = name
#return that item
return itm


Then you can use this in your code:

g = add_new(obj.testgroup.render_groups, "Name")
#->g.name= "Name"