Part 2 of 2 Parts Part 1 introduced some problems discovered with DN handling in FreeIPA. Part 2 discusses proposed utility classes which eliminate the problems described in Part 1 and how to use them. Introduction: ------------- The new module ipalib/dn.py introduces 3 new classes: DN, RDN and AVA. The module and each class have extensive documentation, you should refer to that for details and clarifications, this is only an overview. In addtion there is a unit test (tests/test_ipalib/test_dn.py) with extensive examples of how the classes can be utilized. Of the 3 new classes (DN, RDN & AVA) you will most likely only need to use DN, you may need to use RDN, and it's unlikely you'll need to use AVA. This is because the classes are aware of each other and try to behave intelligently on behalf of the programmer. For example if you supply a pair to a DN object it will interpret this as a single valued RDN and construct a new RDN object, in turn the RDN object will construct a new AVA object passing it the pair and the RDN object will add the new AVA to itself. Then the DN object will add the new RDN to itself. dn = DN(('cn', 'Bob')) # This is exactly equivalent to dn = DN(RDN(AVA('cn', 'Bob'))) The DN, RDN and AVA objects have all the intuitive Pythonic behavior you would expect. Recall a DN is an ordered sequence of RDN's and a RDN is a set of AVA's, thus they are containers and all of Python's container and iteration operators are available to you. Basic attr, value access: ------------------------- AVA's have an attr and value property, thus if you have an AVA # Get the attr and value ava.attr -> u'cn' ava.value -> u'Bob' # Set the attr and value ava.attr = 'cn' ava.value = 'Bob' Since RDN's are assumed to be single valued, exactly the same behavior applies to an RDN. If the RDN is multi-valued then the attr property returns the attr of the first AVA, likewise for the value. # Get the attr and value rdn.attr -> u'cn' rdn.value -> u'Bob' # Set the attr and value rdn.attr = 'cn' rdn.value = 'Bob' Also RDN's can be indexed by name or position (see the RDN class doc for details). rdn['cn'] -> u'Bob' rdn[0] -> AVA('cn', 'Bob') Basic container & iteration operations: --------------------------------------- A DN is a sequence of RDN's, as such any of Python's container operators can be applied to a DN in a intuitive way. # How many RDN's in a DN? len(dn) # WARNING, this a count of RDN's not how characters there are in the # string representation the dn, instead that would be: len(str(dn)) # Iterate over each RDN in a DN for rdn in dn: # Get the first RDN in a DN dn[0] -> RDN('cn', 'Bob') # Get the value of the first RDN in a DN dn[0].value -> u'Bob' # Get the value of the first RDN by indexing by attr name dn['cn'] -> u'Bob' # WARNING, when a string is used as an index key the FIRST RDN's value # in the sequence whose attr matches the key is returned. Thus if you # have a DN like this "cn=foo,cn=bar" then dn['cn'] will always return # 'foo' even though there is another attr with the name 'cn'. This is # almost always what the programmer wants. See the class doc for how # you can override this default behavior and get a list of every value # whose attr matches the key. # Set the first RDN in the DN (all are equivalent) dn[0] = ('cn', 'Bob') dn[0] = ['cn', 'Bob'] dn[0] = RDN('cn', 'Bob') dn[0].attr = 'cn' dn[0].value = 'Bob' # Get the first two RDN's using slices dn[0:2] # Get the last two RDN's using slices dn[-2:] # Get a list of all RDN's using slices dn[:] # Set the 2nd and 3rd RDN using slices (all are equivalent) dn[1:3] = ('cn', 'Bob), ('dc', 'redhat.com') dn[1:3] = RDN('cn', 'Bob), RDN('dc', 'redhat.com') String representations and escapes: ----------------------------------- # To get an RFC compliant string representation of a DN, RDN or AVA # simply call str() on it or evaluate it in a string context. str(dn) -> 'cn=Bob,dc=redhat.com' # When working with attr's and values you do not have to worry about # escapes, simply use the raw unescaped string in a natural fashion. rdn = RDN('cn', 'r,w') # Thus: rdn.value == 'r,w' -> True # But: str(rdn) == 'cn=r,w' -> False # Because: str(rdn) -> 'cn=r\2cw' or 'cn='r\,w' # depending on the underlying LDAP library Equality and Comparing: ----------------------- # All DN's, RDN's and AVA's support equality testing in an intuitive # manner. dn1 = DN(('cn', 'Bob')) dn2 = DN(RDN('cn', 'Bob')) dn1 == dn2 -> True dn1[0] == dn2[0] -> True dn1[0].value = 'Bobby' dn1 == dn2 -> False # See the class doc for how DN's, RDN's and AVA's compare # (e.g. cmp()). The general rule is for objects supporting multiple # values first their lengths are compared, then if the lengths match # the respective components of each are pair-wise compared until one # is discovered to be non-equal Concatenation and In-Place Addition: ------------------------------------ # DN's and RDN's can be concatenated. # Return a new DN by appending the RDN's of dn2 to dn1 dn3 = dn1 + dn2 # Append a RDN to DN's RDN sequence (all are equivalent) dn += ('cn', 'Bob') dn += RDN('cn', 'Bob') # Append a DN to an existing DN dn1 += dn2 Advanced Object Construction: ----------------------------- The constructors for DN, RDN & AVA all take a sequence for greatest flexibility. You can mix types in the sequence. The constructor logic has a stong preference for building RDN's from pairs. Since an AVA's attr & value MUST be a basestring the constructor will look for adjacent basestrings in the sequence starting at the beginning of the sequence. For every pair of strings it finds it will take the first string as an attr and the second string a value and form an AVA or RDN from them depending on context. If a basestring is found which is not adjacent to another basestring it will be interpreted as DN string syntax which will be parsed into it's individual components with each component appropriately added (note, try to avoid using DN string syntax, it has many of the problems these classes were designed to avoid and there is seldom a reason to use it because of the rich features of these classes). Other type objects in the sequence will have their contents added. A 2-member tuple or list will be interpreted as a pair, AVA, RDN and DN objects add their contents. Any other type in the sequence will throw a Type exception. # Construct a DN with 2 RDN's cn=Bob and ou=People dn = DN('cn', 'Bob', 'ou', 'people') # Same thing, but 2nd RDN is an RDN object dn = DN('cn', 'Bob', RDN('ou', 'people')) # Same thing, but 2nd RDN is an DN object dn = DN('cn', 'Bob', DN('ou', 'people')) # Same thing, but 2nd RDN is a DN syntax string (avoid this) dn = DN('cn', 'Bob', 'ou=people') # Same thing, but parameter is DN syntax string (avoid this) dn = DN('cn=Bob,ou=people') # WARNING, this won't work. Why? The first two strings will be # interpretted as an pair, not a DN syntax string followed # by an pair. Try to avoid DN syntax strings. dn = DN('cn=Bob', 'ou', 'people') # WARNING, this won't work. Why? The first two strings will be # interpretted as an pair, not 2 DN syntax strings. # Try to avoid DN syntax strings. dn = DN('cn=Bob', 'ou=people') # Base and Container example: basedn = DN('dc=example,dc=com') container_user = DN('cn', 'users', 'cn', 'accounts') foo_dn = DN('cn', 'foo', container_user, basedn) # Note, all these are equivalent: container_user = DN('cn', 'users', 'cn', 'accounts') container_user = DN(('cn', 'users'), ('cn', 'accounts')) container_user = DN(['cn', 'users'], ['cn', 'accounts']) container_user = DN(RDN('cn', 'users'), RDN('cn', 'accounts'))