Wednesday, December 7, 2011

Test-driven Development: Implementing TypedList (part 3)

I was also going to undertake some research into the methods that TypedList needed to override or otherwise implement in order to make it work the way it needs to. That research leads me to believe that there are a total of 8 methods that I need to be concerned with:

  • __init__: the object constructor has to check any items supplied to it for type-restrictions, but if there are no invalid types provided, it can call list.__init__ and return whatever comes back from that;
  • __add__: called when concatenating two lists (in this case). I have to assume that any items provided are suspect until proven otherwise. I also couldn't determine how many times this method gets called in cases of multiple additions;
  • __iadd__: called when concatenating with +=, same assumptions as for __add__;
  • __setitem__: called when assigning a value to a specific position within the list. The value is treated as a non-iterable (e.g., passing a list results in a list in the position of the original list);
  • __setslice__: Called when assigning values to a slice, items being set need to be type-checked;
  • append: Called when appending values to the list, items added need to be type-checked;
  • insert: Called when inserting a value to a specific position in the list, the value needs to be type-checked;
  • extend: Called to extend items to the end of the list, same drill;
This list of methods was mostly generated by looking at the documentation-sources noted in the last post, but since I wasn't sure exactly what some of the operational parameters for them were (whether they could handle single items only, iterable items, etc., and how they would deal with unexpected types), I also hacked up a quick little test-script to provide some visibility into what exactly happened when various operations and list-methods were called:

class MyList( list, object ):

    def __init__( self, iterable=None ):
        print "MyList.__init__( %s )" % ( iterable )
        list.__init__( self, iterable )
    def __add__( self, items ):
        print "MyList.__add__( %s )" % ( items )
        return list.__add__( self, items )
    def __iadd__( self, items ):
        print "MyList.__iadd__( %s )" % ( items )
        return list.__iadd__( self, items )
    def __setitem__( self, index, item ):
        print "MyList.__setitem__( %s, %s )" % ( index, item )
        return list.__setitem__( self, index, item )
    def __setslice__( self, start, end, items ):
        print "MyList.__setslice__( %s, %s, %s )" % ( start, end, items )
        return list.__setslice__( self, start, end, items )
    def append( self, item ):
        print "MyList.append( %s )" % ( item )
        return list.append( self, item )
    def insert( self, index, item ):
        print "MyList.insert( %s, %s )" % ( index, item )
        return list.insert( self, index, item )
    def extend( self, items ):
        print "MyList.extend( %s )" % ( items )
        return list.extend( self, items )

tester = MyList( ['eek'] )                # uses __init__
tester += [ 'ook' ]                       # uses __iadd__
tester.append( 'eek' )                    # uses append
tester.insert( 1, 'OOOOK!' )              # uses insert
tester[1:2] = [ 'spam', 'eggs' ]          # uses __setslice__
tester += [ 'oooo', 'ooooo!' ]            # uses __iadd__
tester[2] = 'bork!'                       # uses __setitem__
tester.extend( [ 'eeee', 'eeee' ] )       # uses extend
tester = tester + [ 'mooo!' ] + [ 'me' ]  # uses __add__
print
print tester

This little script provided a lot of insight, actually - Though I'd seen some of the documentation for __add__ and __iadd__, for example, it never really stuck in my head that these were the methods that were called when + and += operators were used.

So, with our unit-tests set, and a solid feel for what the code needed to override from list, TypedList was pretty easily written:

TypedList (Nominal final class):

class TypedList( BaseTypedCollection, list ):
    """Represents a strongly-typed List."""

    ##################################
    # Class Attributes               #
    ##################################

    ##################################
    # Class Property-Getter Methods  #
    ##################################

    ##################################
    # Class Property-Setter Methods  #
    ##################################

    ##################################
    # Class Property-Deleter Methods #
    ##################################

    ##################################
    # Class Properties               #
    ##################################

    ##################################
    # Object Constructor             #
    ##################################

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'memberTypes', None, '(Iterable, required) An iterable collection of item-types to be allowed as members in the list.' )
    @DocumentArgument( 'argument', 'iterable', None, '(Iterable, required) An iterable collection of item-values to populate the TypedList with at creation.' )
    def __init__( self, memberTypes, iterable=None ):
        """Object constructor."""
        # Nominally final: Don't allow any class other than this one
        if self.__class__ != TypedList:
            raise NotImplementedError( 'TypedList is (nominally) a final class, and is not intended to be derived from.' )
        self._SetMemberTypes( memberTypes )
        list.__init__( self )
        if iterable != None:
            for item in iterable:
                self.append( item )

    ##################################
    # Object Destructor              #
    ##################################

    ##################################
    # Class Methods                  #
    ##################################

    ##################################
    # Methods overridden from list   #
    ##################################

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def __add__( self, items ):
        """Called when adding items to the list with a "+" (e.g., TL + items)."""
        if not isinstance( items, list ):
            raise TypeError( 'TypedList.__add__ expects a list (or list-derived) instance of values.' % ( self._memberTypes ) )
        for item in items:
            if not self.IsValidMemberType( item ):
                raise TypeError( 'TypedList.__add__ expects a list (or list-derived) instance of values of any of %s. %s is not valid.' % ( self._memberTypes, items ) )
        return list.__add__( self, items )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def __iadd__( self, items ):
        """Called when adding items to the list with a "+=" (e.g., TL += item)."""
        if not isinstance( items, list ):
            raise TypeError( 'TypedList.__iadd__ expects a list (or list-derived) instance of values.' % ( self._memberTypes ) )
        for item in items:
            if not self.IsValidMemberType( item ):
                raise TypeError( 'TypedList.__iadd__ expects a list (or list-derived) instance of values of any of %s. %s is not valid.' % ( self._memberTypes, items ) )
        return list.__iadd__( self, items )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def __setitem__( self, index, item ):
        """Called when setting the value of a specified location in the list (TL[n] = item)."""
        if not self.IsValidMemberType( item ):
            raise TypeError( 'TypedList.__setitem__ expects an instance of any of %s.' % ( self._memberTypes ) )
        return list.__setitem__( self, index, item )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def __setslice__( self, start, end, items ):
        """Called when setting the value(s) of a slice-location in the list (TL[1:2] = item)."""
        for item in items:
            if not self.IsValidMemberType( item ):
                raise TypeError( 'TypedList.__setslice__ expects an iterable of instances of any of %s.' % ( self._memberTypes ) )
        return list.__setslice__( self, start, end, items )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def append( self, item ):
        """Called when appending items to the list."""
        if not self.IsValidMemberType( item ):
            raise TypeError( 'TypedList.append expects an instance of any of %s.' % ( self._memberTypes ) )
        return list.append( self, item )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def extend( self, items ):
        """Called when extending items to a the end of the list."""
        if not isinstance( items, list ):
            raise TypeError( 'TypedList.extend expects a list (or list-derived) instance of values.' % ( self._memberTypes ) )
        for item in items:
            if not self.IsValidMemberType( item ):
                raise TypeError( 'TypedList.extend expects a list (or list-derived) instance of values of any of %s. %s is not valid.' % ( self._memberTypes, items ) )
        return list.extend( self, items )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def insert( self, index, item ):
        """Called when inserting items to a specified location in the list (TL[1] = item)."""
        if not self.IsValidMemberType( item ):
            raise TypeError( 'TypedList.insert expects an instance of any of %s.' % ( self._memberTypes ) )
        return list.insert( self, index, item )

__all__ += [ 'TypedList' ]

Ultimately, as with TypedDictionary earlier, there's not a huge amount of code to be written. Python's built-ins and the methods specific to adding items to or replacing items in a list were the only methods that had to be overridden, and there were only two patterns for those modifications:

  • If the method expected a list-type (e.g., __add__, __iadd__, __setslice__ and extend):
    • The supplied items are checked to make sure that they are derived from a list. This allows list-types and extensions of list types (like Typedlist) to be used.
    • Once the basic supplied type is successfully checked, the items in it have to be individually type-checked (using IsValidMemberType), and if any of them are invalid, raise an error.
    • If everything is good, then call (and return) the parent list-class method with the supplied value(s).
  • If the method expects a non-list instance value, then all that need be done is type-check the value (again, with IsValidMemberType):
    • If it's invalid, raise a TypeError.
    • Otherwise, call (and return) the parent list-class method with the supplied value.

Line(s)
34-35
Treat TypedList as a final class, and don't allow extension.
36
Set the member-types allowed by the instance.
37
Makes sure that the instance calls a basic list-constructor. We can (and should) ignore any supplied instance-values in the iterable argument when calling the parent constructor, though, because we want to use the overridden append method to build out the instance's list of values.
38-40
Checks to see if the constructor is being passed an iterable collection of values to populate the instance with, loops over them if they are supplied, and appends them to the instance's values one by one.
I could've just as easily looped over any supplied iterable values in the constructor, raising an error at the first bad type supplied, and then called the parent constructor with the iterable value. I have a gut-level feeling, however, that taking that approach could raise issues in the future, and since I'd've had to iterate over the values anyway, it just seemed safer to implement the way I did. I can't justify it, but I'm going to trust my instincts.
55-62, 65-72, 82-87, 97-104
Uses the list-checking pattern noted above to override __add__, __iadd__, __setslice__ and extend, respectively.
75-81, 90-94, 107-111
Uses the value-checking pattern noted above to override __setitem__, append and insert, respectively.

No comments:

Post a Comment