Showing posts with label ResultSet. Show all posts
Showing posts with label ResultSet. Show all posts

Friday, December 16, 2011

The DataConnectors module (part 5)

Looking at the requirements (both functional and logical) for ResultSet, most of them are already accounted for simply by making ResultSet derive from TypedList, and hard-setting the member-types to Record. Consider:

  • A result-set is a sequence of records - Check;
  • A result-set can be iterated over, and the fields of any individual row or record can be accessed using a syntax along the lines of result[ i ].FieldName, or within an iteration over results, results.FieldName - Check;
  • The individual rows, once populated, are immutable - Check;
But there are other requirements, more based on the logical expectation(s) of a record or row from a query:
  • Each row or record of a result-set should have the same structure (e.g., the same field-names as attributes), even if they have a null or None value;
  • Once populated, the sequence of rows cannot be altered, including adding new rows, removing existing ones, or changing any row-values, either singly or as part of a slice;
These requirements haven't been dealt with yet, since they reside at the ResultSet level of the stack (and they may carry back up to the IsResultSet nominal interface as well). But given the functionality we know of in TypedList, and the object-locking-pattern that was used in Record, the basic mechanisms aren't difficult to implement. Since there will be overrides of many (all?) of the TypedList methods in order to check for lock-state on the record-set object, as well as to check the field-structure of the records, each of these methods has to be tested at the ResultSet level, just as they were for TypedList, though... And if you recall, there was a substantial amount of testing that had to happen there, for all of the various list-behaviors...

ResultSet (Nominal final class):

class ResultSet( IsResultSet, TypedList, object ):
    """Class doc-string"""

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

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

    def _GetFieldNames( self ):
        return self.__fieldNames

    def _GetRowCount( self ):
        return len( self )

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

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

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

    FieldNames = property( _GetFieldNames, None, None, IsResultSet.FieldNames.__doc__ )
    RowCount = property( _GetRowCount, None, None, IsResultSet.RowCount.__doc__ )

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'results', None, '(iterable collection of dictionaries) The sequence of rows to populate in the result-set.' )
    def __init__( self, results ):
        """Object constructor."""
        # Nominally final: Don't allow any class other than this one
        if self.__class__ != ResultSet:
            raise NotImplementedError( 'ResultSet is (nominally) a final class, and is not intended to be derived from.' )
        TypedList.__init__( self, [ Record ] )
        self.__fieldNames = []
        self.__resultsLocked = False
        for item in results:
            if not isinstance( item, Record ):
                item = Record( **item )
            if self.__fieldNames == []:
                self.__fieldNames = item.FieldNames
            else:
                if self.__fieldNames != item.FieldNames:
                    raise AttributeError( '%s Error: Expected a row with %s fields, but a row with %s fields was present instead' % ( self.__class__.__name__, self.__fieldNames, item.FieldNames ) )
            self.append( item )
        self.__resultsLocked = True

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

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

    ##################################
    # TypedList Methods overridden   #
    ##################################

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'items', None, '(List or derived, required) The list of items to add to the current list.' )
    def __add__( self, items ):
        """Called when adding items to the list with a "+" (e.g., TL + items)."""
        if self.__resultsLocked:
            raise AttributeError( '%s __add__: the %s is locked and cannot be modified' % ( self.__class__.__name__, self.__class__.__name__ ) )
        # check items
        for item in items:
            if item.Keys != self.__fieldNames:
                raise ValueError( '%s __add__: %s is not a Record instance with fields %s' % ( self.__class__.__name__, item, self.__fieldNames ) )
        TypedList.__add__( self, items )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'item', None, '(List or derived, required) The list of item to add to the current list.' )
    def __delitem__( self, item ):
        """Called when deleting an item from the list."""
        if self.__resultsLocked:
            raise AttributeError( '%s __delitem__: the %s is locked and cannot be modified' % ( self.__class__.__name__, self.__class__.__name__ ) )
        TypedList.__delitem__( self, item )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'items', None, '(List or derived, required) The list of items to append to the current list.' )
    def __iadd__( self, items ):
        """Called when adding items to the list with a "+=" (e.g., TL += item)."""
        if self.__resultsLocked:
            raise AttributeError( '%s __iadd__: the %s is locked and cannot be modified' % ( self.__class__.__name__, self.__class__.__name__ ) )
        # check items
        for item in items:
            if item.Keys != self.__fieldNames:
                raise ValueError( '%s __iadd__: %s is not a Record instance with fields %s' % ( self.__class__.__name__, item, self.__fieldNames ) )
        TypedList.__iadd__( self, items )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'index', None, '(Integer, required) The index-location to set the item in.' )
    @DocumentArgument( 'argument', 'item', None, '(Any, required) The item to set in the specified list-location.' )
    def __setitem__( self, index, item ):
        """Called when setting the value of a specified location in the list (TL[n] = item)."""
        if self.__resultsLocked:
            raise AttributeError( '%s __setitem__: the %s is locked and cannot be modified' % ( self.__class__.__name__, self.__class__.__name__ ) )
        if item.FieldNames != self.__fieldNames:
            raise ValueError( '%s __setitem__: %s is not a Record instance with fields %s' % ( self.__class__.__name__, item, self.__fieldNames ) )
        TypedList.__setitem__( self, index, item )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'start', None, '(Integer, required) The starting position of the slice to set the supplied items in.' )
    @DocumentArgument( 'argument', 'end', None, '(Integer, required) The ending position of the slice to set the supplied items in.' )
    @DocumentArgument( 'argument', 'items', None, '(List or derived, required) The list of items to set in the specified slice-location.' )
    def __setslice__( self, start, end, items ):
        """Called when setting the value(s) of a slice-location in the list (TL[1:2] = item)."""
        if self.__resultsLocked:
            raise AttributeError( '%s __setslice__: the %s is locked and cannot be modified' % ( self.__class__.__name__, self.__class__.__name__ ) )
        # check items
        for item in items:
            if item.FieldNames != self.__fieldNames:
                raise ValueError( '%s __setslice__: %s is not a Record instance with fields %s' % ( self.__class__.__name__, item, self.__fieldNames ) )
        TypedList.__setslice__( self, start, end, items )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'item', None, '(Any, required) The item to append.' )
    def append( self, item ):
        """Called when appending items to the list."""
        if self.__resultsLocked:
            raise AttributeError( '%s error: the %s is locked and cannot be modified' % ( self.__class__.__name__, self.__class__.__name__ ) )
        if item.FieldNames != self.__fieldNames:
            raise ValueError( '%s append: %s is not a Record instance with fields %s' % ( self.__class__.__name__, item, self.__fieldNames ) )
        TypedList.append( self, item )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'items', None, '(List or derived, required) The list of items to extend to the current list.' )
    def extend( self, items ):
        """Called when extending items to a the end of the list."""
        if self.__resultsLocked:
            raise AttributeError( '%s error: the %s is locked and cannot be modified' % ( self.__class__.__name__, self.__class__.__name__ ) )
        # check items
        for item in items:
            if item.FieldNames != self.__fieldNames:
                raise ValueError( '%s error: %s is not a Record instance with fields %s' % ( self.__class__.__name__, item, self.__fieldNames ) )
        TypedList.extend( self, items )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'index', None, '(Integer, required) The index-location to insert the item in.' )
    @DocumentArgument( 'argument', 'item', None, '(Any, required) The item to insert in the specified list-location.' )
    def insert( self, index, item ):
        """Called when inserting items to a specified location in the list (TL[1] = item)."""
        if self.__resultsLocked:
            raise AttributeError( '%s error: the %s is locked and cannot be modified' % ( self.__class__.__name__, self.__class__.__name__ ) )
        if item.FieldNames != self.__fieldNames:
            raise ValueError( '%s append: %s is not a Record instance with fields %s' % ( self.__class__.__name__, item, self.__fieldNames ) )
        TypedList.insert( self, index, item )

__all__ += [ 'ResultSet' ]
Line(s)
12-16, 30-31
Standard read-only property set-up...
37-56
Object constructor:
42-43
Enforcing a nominally-final class for the object.
44
Making sure that TypedList functionality is available for instances.
45-46
Setting instance-values for the two new attributes of the class.
47-55
Populating the instance with the results provided:
48-49
The class should, ideally, be able to handle results as either an iterable of Record instances, or an iterable of dictionary instances - if the item is a dictionary, it will need to be made into a Record instance.
I feel like I've neglected type-checking the item at this point, making sure that it's a dictionary, but I don't know that it would really make a difference. Instantiation of a Record with anything other than a dictionary or a keyword argument list raises an error on it's own, so I don't think explicitly checking makes enough of a difference to implement that. At least not yet.
50-54
This is where the ResultSet's field-names are set, on the first pass through the loop, and where the structural consistency of subsequent records is checked.
55
If no errors were raised by the time that we reach this point, then the item is appended to the underlying list.
56
Locks the object, preventing futher modification.
70-158
The various built-ins and methods here all follow more or less the same pattern:
  • Check to see if the object is locked, and raise AttributeError if it is;
  • Check for structural consistency with the established field-names for the item or items supplied;
  • If no errors are raised, call the parent method from TypedList to perform the actual operation.
Ultimately, they're all just checking to see if the operation is allowed before performing it - nothing too fancy, though some cases may look a bit more convoluted than is perhaps typical.
82-88
Since the records in a ResultSet should be immutable, deletion has to be prevented as well.
This is something that I overlooked in Record, so I'll need to get that in place there as well.

Unit-tests

class ResultSetDerived( ResultSet ):
        def __init__( self, results ):
            ResultSet.__init__( self, results )
    
    class testResultSet( unittest.TestCase ):
        """Unit-tests the ResultSet class."""

        def getBadRecord( self ):
            return { 'Number1':3, 'Name':'ook eek', 'IsValid':True }

        def getGoodRecord( self ):
            return { 'Number':3, 'Name':'ook eek', 'IsValid':True }

        def getBadValues( self ):
            return [ 1, 'ook', True, None, {} ]

        def getGoodValues( self ):
            return [
                { 'Number':1, 'Name':'ook', 'IsValid':True },
                { 'Number':2, 'Name':'eek', 'IsValid':False },
                { 'Number':None, 'Name':None, 'IsValid':None },
                ]

        def getInconsistentValues( self ):
            return [
                { 'Number':1, 'Name':'ook', 'IsValid':True },
                { 'Number':1, 'IsValid':True },
                { 'Number':2, 'Name':'eek' },
                { 'Name':'eek', 'IsValid':False },
                { 'Number':None },
                { 'IsValid':None },
                ]

        def setUp( self ):
            pass
    
        def tearDown( self ):
            pass
        
        def testFinal( self ):
            """Testing final nature of the ResultSet class."""
            try:
                testObject = ResultSetDerived( self.getGoodValues() )
                self.fail( 'ResultSet is nominally a final class, and should not be extendable.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'Instantiating an object derived from ResultSet should raise NotImplementedError, by %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testConstruction( self ):
            """Testing construction of the ResultSet class."""
            testObject = ResultSet( self.getGoodValues() )
            # TODO: Test values...
            # Test inconsistent values - all should raise errors
            inconsistentValues = self.getInconsistentValues()
            # Test creation with an iterable
            try:
                testObject = ResultSet( inconsistentValues )
                self.fail( 'Construction with inconsistent record-structures should raise an error.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Construction with inconsistent record-structures should raise AttributeError, but %s was raised instead:\n  %s.' % ( error.__class__.__name__, error ) )

        def testPropertyCountAndTests( self ):
            """Testing the properties of the ResultSet class."""
            items = getMemberNames( ResultSet )[0]
            actual = len( items )
            expected = 3
            self.assertEquals( expected, actual, 'ResultSet is expected to have %d properties to test, but %d were dicovered by inspection.' % ( expected, actual ) )
            for item in items:
                self.assertTrue( HasTestFor( self, item ), 'There should be a test for the %s property (test%s), but none was identifiable.' % ( item, item ) )

        def testMethodCountAndTests( self ):
            """Testing the methods of the ResultSet class."""
            items = getMemberNames( ResultSet )[1]
            actual = len( items )
            expected = 4
            self.assertEquals( expected, actual, 'ResultSet is expected to have %d methods to test, but %d were dicovered by inspection.' % ( expected, actual ) )
            for item in items:
                self.assertTrue( HasTestFor( self, item ), 'There should be a test for the %s method (test%s), but none was identifiable.' % ( item, item ) )

        # Test list behavior
        def testListBehavior( self ):
            """Unit-tests list-like behavior of ResultSet."""
            # Test good values - all should raise errors
            goodValues = self.getGoodValues()
            testObject = ResultSet( goodValues )
            validRecord = Record( **self.getGoodRecord() )
            # Test L = L + i structure
            try:
                testObject + [ validRecord ]
                self.fail( 'Once a ResultSet has been populated, it should be immutable.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Attempting to modify a ResultSet once it\'s been populated should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            # Test L = L += i structure
            try:
                testObject += [ validRecord ]
                self.fail( 'Once a ResultSet has been populated, it should be immutable.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Attempting to modify a ResultSet once it\'s been populated should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            # Test L.append equivalent
            try:
                testObject.append( validRecord )
                self.fail( 'Once a ResultSet has been populated, it should be immutable.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Attempting to modify a ResultSet once it\'s been populated should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            # Test L.extend equivalent
            try:
                testObject.extend( [ validRecord ] )
                self.fail( 'Once a ResultSet has been populated, it should be immutable.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Attempting to modify a ResultSet once it\'s been populated should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            # Test L.insert equivalent
            try:
                testObject.insert( 0, validRecord )
                self.fail( 'Once a ResultSet has been populated, it should be immutable.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Attempting to modify a ResultSet once it\'s been populated should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            # Test L[index] = value equivalent
            try:
                testObject[ 0 ] = validRecord
                self.fail( 'Once a ResultSet has been populated, it should be immutable.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Attempting to modify a ResultSet once it\'s been populated should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            # Test L[slice] = [ i, j ] equivalent
            try:
                testObject[ 0:1 ] = validRecord
                self.fail( 'Once a ResultSet has been populated, it should be immutable.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Attempting to modify a ResultSet once it\'s been populated should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            # Test del L[i]
            try:
                del testObject[ 0 ]
                self.fail( 'Once a ResultSet has been populated, it should be immutable.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Attempting to modify a ResultSet once it\'s been populated should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        # Test Properties

        def testMemberTypes( self ):
            """Unit-tests the MemberTypes property of the ResultSet class."""
            self.assertEquals( TypedList.MemberTypes, BaseTypedCollection.MemberTypes, 'TypedList should inherit the MemberTypes property of BaseTypedCollection.' )

        def testFieldNames( self ):
            """Unit-tests the FieldNames property of the ResultSet class."""
            goodValues = self.getGoodValues()
            testObject = ResultSet( goodValues )
            expected = Record( **goodValues[ 0 ] ).FieldNames
            self.assertEquals( testObject.FieldNames, expected, 'The field-names of a ResultSet should be the same as the field-names of it\'s Record-instances.' )

        def testRowCount( self ):
            """Unit-tests the RowCount property of the ResultSet class."""
            goodValues = self.getGoodValues()
            testObject = ResultSet( goodValues )
            self.assertEquals( testObject.RowCount, len(goodValues), 'The number of rows in RowCount should be the same as the number of items presented at construction.' )

        # Test Methods

        def testappend( self ):
            """Unit-tests the append method of the ResultSet class."""
            pass # This is tested in testListBehavior, above.

        def testextend( self ):
            """Unit-tests the extend method of the ResultSet class."""
            pass

        def testinsert( self ):
            """Unit-tests the insert method of the ResultSet class."""
            pass # This is tested in testListBehavior, above.

        def testIsValidMemberType( self ):
            """Unit-tests the IsValidMemberType method of the ResultSet class."""
            self.assertEquals( TypedList.IsValidMemberType, BaseTypedCollection.IsValidMemberType, 'TypedList should inherit the IsValidMemberType method of BaseTypedCollection.' )

    testSuite.addTests( unittest.TestLoader().loadTestsFromTestCase( testResultSet ) )

I'm going to skip commenting on the unit-tests this time around - they are all either very straightforward (I believe), or repeats of previous test-structures. The only new thing here, really, is the use of a couple of methods (getBadRecord, getGoodRecord, getGoodValues, and getInconsistentValues) used to generate results for creation of RecordSet instances for the various test-methods.

The unit-tests for RecordSet also revealed a flaw in Record: I'd forgotten to initialize a local __fieldNames attribute, and as a result, the value being set in that property for the first instance was shared across all instances (it was behaving as a static attribute). The changes to Record and it's unit-tests are:

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'keyword', 'properties', 'variable', 'Key/value pairs providing the record\'s field-names and values of those fields.' )
    def __init__( self, **properties ):
        """Object constructor."""
        # Nominally final: Don't allow any class other than this one
        if self.__class__ != Record:
            raise NotImplementedError( 'Record is (nominally) a final class, and is not intended to be derived from.' )
        self.__fieldNames = []
        for theKey in properties:
            self.__setattr__( theKey, properties[ theKey ] )
            if not theKey in self.__fieldNames:
                self.__fieldNames.append( theKey )
        self.__recordIsLocked = True

# break

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'name', None, '(String, required) The name of the attribute to be set.' )
    @DocumentArgument( 'argument', 'value', None, '(Any type, required) The value to be set in the named attribute.' )
    def __setattr__( self, name, value ):
        """Object-attribute setter."""
        if name == '_Record__recordIsLocked' and self.__recordIsLocked != False:
            raise AttributeError( 'The record is locked, and cannot be unlocked' )
        if self.__recordIsLocked:
            raise AttributeError( 'The record has been locked, and the "%s" attribute cannot be set.' % ( name ) )
        if hasattr( self, name ) and not name in [ '_Record__recordIsLocked', '_Record__fieldNames' ]:
            raise AttributeError( 'The "%s" attribute already exists, and cannot be reset.' % ( name ) )
        return object.__setattr__( self, name, value )

The changes here are:

Line(s)
8
self.__fieldNames is being explicitly set to an empty list
26
Explicitly checking the property-name against both _Record__recordIsLocked and _Record__fieldNames, in order to allow either to be set if the object isn't locked yet.
This feels like it may not be complete, so I may very well come back to it later...

        def testFieldNames( self ):
            """Unit-tests the FieldNames property of the Record class."""
            testProperties = { 'Property1':1, 'Property2':'ook', 'Property3':True }
            testProperties2 = { 'Property':1 }
            testObject = Record( **testProperties )
            self.assertEquals( testObject.FieldNames, testProperties.keys() )
            try:
                testObject.Keys = 'ook'
                self.fail( 'A Record\'s FieldNames cannot be altered.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Changes to a record\'s FieldNames should raise AttributeError, bu %s was raised instead:\n  %s ' % ( error.__class__.__name__, error ) )
            testObject = Record( **testProperties2 )
            self.assertEquals( testObject.FieldNames, testProperties2.keys(), 'The field-names for each record should be local to the instance' )

The change here is the addition of the last two lines - since each Record instance should have set it's FieldNames property independently, the second testObject should have the same keys as the testProperties2 dictionary. If the attribute were not explicitly set locally, it would have the keys from testProperties instead.

And that's the last of the "simple" concrete classes that weren't waiting on research. At least until the need for another one rears it's head. Next (finally), I'll get back to defining where query-execution takes place - you may remember that I set that aside to do some research in my previous post. I've done that research, and I can move on to implementation now...

Wednesday, November 30, 2011

The DataConnectors module (part 2)

The next two items tho work through are the IsQuery and IsResultSet nominal interfaces. With the sort of structure established with IsDataConnector in the last post, all that really need be done for either of these is to figure out what functionality (and properties) we're eventually going to want in the classes and objects derived from these interfaces - so it's design time again! This will fill out two more items in the class diagram.

Without going into too much detail about how the queries, results and connectors actually interact, the process for retrieving data with a query using the DataConnectors stack will look something like this:

  • A connector object (derived from BaseDataConnector, and in turn from IsDataConnector) will be created, pointing at some back-end database.
  • A query-object (derived from IsQuery) will be created with the SQL to be executed against the datasource, and the datasource itself. Whether the query-object actually executes the SQL when instatiated, or when the results are first requested is still to be determined, but it will have a Results property that will allow access to the results of the query against the database.
  • When the results are requested from the query-object, they will be returned as a list of dumb data-objcets, each having an attribute for each field in the rows returned for the result-set, named after the field-name, and containing the value retrieved. These attributes will be immutable.
This structure should allow a good balance between flexibility, ease of use, and ease of maintaining data-access code.

Code-share: dl.dropbox.com/u/1917253/site-packages/DataConnectors.py

IsQuery (Nominal interface):

class IsQuery( object ):
    """Class doc-string."""

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

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

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

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

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

    Datasource = property( None, None, None, 'Gets the data-source (an IsDataConnector instance) that the query was or will be executed against.' )
    Results = property( None, None, None, 'Gets the results returned by execution of the query against the object\'s Datasource.' )
    Sql = property( None, None, None, 'Gets the SQL that has been executed against the object\'s Datasource.' )

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'datasource', None, '(IsDataConncetor, required) The IsDataConnector instance that the query\'s SQL will be executed against.' )
    @DocumentArgument( 'argument', 'sql', None, '(String, required) The SQL that will be executed against the supplied datasource.' )
    def __init__( self, datasource, sql ):
        """Object constructor."""
        # Nominally abstract: Don't allow instantiation of the class
        if self.__class__ == IsQuery:
            raise NotImplementedError( 'IsQuery is (nominally) an interface, and is not intended to be instantiated.' )

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

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

__all__ += [ 'IsQuery' ]

There really isn't much to say about IsQuery: it's simple, it has only a few properties, and there aren't any method-stubs to it. In fact, the only significant change here is that the properties have been documented in the interface. We'll use this documentation-string later, when we define the Query class. The unit-tests are equally straightforward:

    class IsQueryDerived( IsQuery ):
        def __init__( self, datasource, sql ):
            IsQuery.__init__( self, datasource, sql )
    
    class testIsQuery( unittest.TestCase ):
        """Unit-tests the IsQuery class."""
    
        def setUp( self ):
            pass
    
        def tearDown( self ):
            pass
        
        def testDerived( self ):
            """Testing abstract nature of the IsQuery class."""
            try:
                testObject = IsQuery( None, None )
                self.fail( 'Instantiation of an IsQuery object should raise a NotImplementedError.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'Instantiation of an IsQuery object should raise a NotImplementedError, but %s was raised instead:\n  %s.' % ( error.__class__.__name__, error ) )
            testObject = IsQueryDerived( None, None )
            self.assertTrue( isinstance( testObject, IsQuery ), 'Objects derived from IsQuery should be instances of IsQuery.' )

        def testPropertyCountAndTests( self ):
            """Testing the properties of the IsQuery class."""
            items = getMemberNames( IsQuery )[0]
            actual = len( items )
            expected = 3
            self.assertEquals( expected, actual, 'IsQuery is expected to have %d properties to test, but %d were dicovered by inspection.' % ( expected, actual ) )
            for item in items:
                self.assertTrue( HasTestFor( self, item ), 'There should be a test for the %s property (test%s), but none was identifiable.' % ( item, item ) )

        def testMethodCountAndTests( self ):
            """Testing the methods of the IsQuery class."""
            items = getMemberNames( IsQuery )[1]
            actual = len( items )
            expected = 0
            self.assertEquals( expected, actual, 'IsQuery is expected to have %d methods to test, but %d were dicovered by inspection.' % ( expected, actual ) )
            for item in items:
                self.assertTrue( HasTestFor( self, item ), 'There should be a test for the %s method (test%s), but none was identifiable.' % ( item, item ) )

        # Test properties

        def testDatasource( self ):
            """Tests the datasource property of the IsQuery interface."""
            testObject = IsQueryDerived( None, None )
            try:
                x = testObject.Datasource
                self.fail( 'IsQuery.Datasource should not be usable as a getter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsQuery.Datasource, used as a getter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                testObject.Datasource = None
                self.fail( 'IsQuery.Datasource should not be usable as a setter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsQuery.Datasource, used as a setter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                del testObject.Datasource
                self.fail( 'IsQuery.Datasource should not be usable as a deleter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsQuery.Datasource, used as a deleter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testResults( self ):
            """Tests the Results property of the IsQuery interface."""
            testObject = IsQueryDerived( None, None )
            try:
                x = testObject.Results
                self.fail( 'IsQuery.Results should not be usable as a getter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsQuery.Results, used as a getter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                testObject.Results = None
                self.fail( 'IsQuery.Results should not be usable as a setter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsQuery.Results, used as a setter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                del testObject.Results
                self.fail( 'IsQuery.Results should not be usable as a deleter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsQuery.Results, used as a deleter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testSql( self ):
            """Tests the Sql property of the IsQuery interface."""
            testObject = IsQueryDerived( None, None )
            try:
                x = testObject.Sql
                self.fail( 'IsQuery.Sql should not be usable as a getter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsQuery.Sql, used as a getter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                testObject.Sql = None
                self.fail( 'IsQuery.Sql should not be usable as a setter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsQuery.Sql, used as a setter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                del testObject.Sql
                self.fail( 'IsQuery.Sql should not be usable as a deleter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsQuery.Sql, used as a deleter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

I didn't provide any commentary on the similarly-structure unit-tests in my last post, for IsDataConnector, so let me go over some of the new items that are common to it and to IsQuery alike:

Line(s)
14-24
The testDerived test-methods, in both cases, are really just intended to prove that the nominal interfaces behave as expected when directly instantiated and when implemented. Since there's not much in the way of "real" functionality associated with either, they shouldn't be instantiated (it would do no good to anyway, since instances would just be dumb objects with properties and methods that raise errors).
26-33, 35-42
The standard testPropertyCountAndTests and testMethodCountAndTests make sure that all properties and method have test-methods associated with them, like usual.
46-69
The testDatasource test-method makes certain that the interface's Datasource property exists, but is not implemented, raising an AttributeError when a get, set or delete operation is attempted against it.
71-94, 96-119
Test-methods, using the same structure as testDatasource, but for the Results and Sql properties of the interface.

Overall, pretty simple, I hope. The interface isn't very complex, so the tests against it shouldn't be too complex either.

The next nominal interface I'm going to delve into is IsResultSet. Before I do, though, I'd like to show the class diagram again, because when I drew it up, there was something that I did that cannot, I think, be done in languages that provide formal interface declarations. Take a moment and look at it again, if you would, paying close attention to the relationships associated with IsResultSet (upper right area):

The tricky bit here, at least as presented on the class diagram, is that IsResultSet, a nominal interface, is derived from TypedList, which is a concrete class. I'm not going to keep that structure (and I'll alter the class diagram accordingly), but this idea is, possibly, worth some discussion.

If that inheritance-relationship were kept, what we would end up with is a nominal interface that contained some actual, concrete implementation. If you're used to interfaces as they're handled in most of the languages that support formal interface declarations, this may set your eye to twitching. But, really, it might not be as bad as you'd think at first blush. In this particular case, since I'm not expecting to need more than one ResultSet class, it almost makes sense to leave the relationship - anything that implemented IsResultSet would then also be, inherently, a TypedList, which is exactly what is needed if we assume that any result-set object will be a list of rows with some additional properties.

Since that's what I'm aiming to accomplish, that doesn't actually feel too bad.

Yes, there are potential encapsulation implications, and it could certainly have implications in how unit-tests are written for IsResultSet and anything that derived from it, but even if it was discovered somewhere down the line that the TypedList/IsResultSet joint inheritance was causing problems, it'd be a simple matter of changing the inheritance there, removing some (now-superfluous) unit-tests for IsResultSet, and moving them to the derived class' unit-tests. That process might be a bit tedious, but it wouldn't be difficult (and I think it would actually be a quick change to make).

If I were 100% confident that there wouldn't be a need to have more than one ResultSet-like class in the stack, I think I'd leave the relationship and it's... odd... inheritance. But I'm not that sure that I won't want or need to have a custom implementation similar to ResultSet somewhere down the line, so I'll break it out.

So, the revised class-diagram looks like this:

Another (minor) advantage to this is that I don't have to stop and implement TypedList right here and now, though I think I'll take care of that soon, since I will need that to be in place before we get to implementation of ResultSet.

IsResultSet (Nominal interface):

class IsResultSet( object ):
    """Class doc-string."""

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

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

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

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

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

    CurrentRow = property( None, None, None, 'Gets the current row of the result-set.' )
    FieldNames = property( None, None, None, 'Gets the names of the fields in each row of the results.' )
    RowCount = property( None, None, None, 'Gets the number of rows in the result-set.' )
    RowIndex = property( None, None, None, 'Gets the index of the current row of the result-set in an iteration.' )

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def __init__( self ):
        """Object constructor."""
        # Nominally abstract: Don't allow instantiation of the class
        if self.__class__ == IsResultSet:
            raise NotImplementedError( 'IsResultSet is (nominally) an interface, and is not intended to be instantiated.' )

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

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

__all__ += [ 'IsResultSet' ]

The only items of note here are the property-definitions (lines 24-27), which are defined the same way the properties for IsQuery were, above.

Unit-tests

    class IsResultSetDerived( IsResultSet ):
        def __init__( self ):
            pass
    
    class testIsResultSet( unittest.TestCase ):
        """Unit-tests the IsResultSet class."""
    
        def setUp( self ):
            pass
    
        def tearDown( self ):
            pass
        
        def testDerived( self ):
            """Testing abstract nature of the IsResultSet class."""
            try:
                testObject = IsResultSet()
                self.fail( 'Instantiation of an IsResultSet object should raise a NotImplementedError.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'Instantiation of an IsResultSet object should raise a NotImplementedError, but %s was raised instead:\n  %s.' % ( error.__class__.__name__, error ) )
            testObject = IsResultSetDerived()
            self.assertTrue( isinstance( testObject, IsResultSet ), 'Objects derived from IsResultSet should be instances of IsResultSet.' )

        def testPropertyCountAndTests( self ):
            """Testing the properties of the IsResultSet class."""
            items = getMemberNames( IsResultSet )[0]
            actual = len( items )
            expected = 4
            self.assertEquals( expected, actual, 'IsResultSet is expected to have %d properties to test, but %d were dicovered by inspection.' % ( expected, actual ) )
            for item in items:
                self.assertTrue( HasTestFor( self, item ), 'There should be a test for the %s property (test%s), but none was identifiable.' % ( item, item ) )

        def testMethodCountAndTests( self ):
            """Testing the methods of the IsResultSet class."""
            items = getMemberNames( IsResultSet )[1]
            actual = len( items )
            expected = 0
            self.assertEquals( expected, actual, 'IsResultSet is expected to have %d methods to test, but %d were dicovered by inspection.' % ( expected, actual ) )
            for item in items:
                self.assertTrue( HasTestFor( self, item ), 'There should be a test for the %s method (test%s), but none was identifiable.' % ( item, item ) )

        # Test properties

        def testCurrentRow( self ):
            """Tests the CurrentRow property of the IsResultSet interface."""
            testObject = IsResultSetDerived()
            try:
                x = testObject.CurrentRow
                self.fail( 'IsResultSet.CurrentRow should not be usable as a getter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsResultSet.CurrentRow, used as a getter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                testObject.CurrentRow = None
                self.fail( 'IsResultSet.CurrentRow should not be usable as a setter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsResultSet.CurrentRow, used as a setter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                del testObject.CurrentRow
                self.fail( 'IsResultSet.CurrentRow should not be usable as a deleter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsResultSet.CurrentRow, used as a deleter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testFieldNames( self ):
            """Tests the FieldNames property of the IsResultSet interface."""
            testObject = IsResultSetDerived()
            try:
                x = testObject.FieldNames
                self.fail( 'IsResultSet.FieldNames should not be usable as a getter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsResultSet.FieldNames, used as a getter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                testObject.FieldNames = None
                self.fail( 'IsResultSet.FieldNames should not be usable as a setter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsResultSet.FieldNames, used as a setter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                del testObject.FieldNames
                self.fail( 'IsResultSet.FieldNames should not be usable as a deleter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsResultSet.FieldNames, used as a deleter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testRowCount( self ):
            """Tests the RowCount property of the IsResultSet interface."""
            testObject = IsResultSetDerived()
            try:
                x = testObject.RowCount
                self.fail( 'IsResultSet.RowCount should not be usable as a getter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsResultSet.RowCount, used as a getter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                testObject.RowCount = None
                self.fail( 'IsResultSet.RowCount should not be usable as a setter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsResultSet.RowCount, used as a setter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                del testObject.RowCount
                self.fail( 'IsResultSet.RowCount should not be usable as a deleter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsResultSet.RowCount, used as a deleter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testRowIndex( self ):
            """Tests the RowIndex property of the IsResultSet interface."""
            testObject = IsResultSetDerived()
            try:
                x = testObject.RowIndex
                self.fail( 'IsResultSet.RowIndex should not be usable as a getter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsResultSet.RowIndex, used as a getter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                testObject.RowIndex = None
                self.fail( 'IsResultSet.RowIndex should not be usable as a setter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsResultSet.RowIndex, used as a setter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                del testObject.RowIndex
                self.fail( 'IsResultSet.RowIndex should not be usable as a deleter.' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'IsResultSet.RowIndex, used as a deleter, should raise AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

These unit-tests are also essentially identical to the ones generated for IsQuery.

A side-note, however, on these unit-tests: Since I had stubbed out all of the interfaces, abstract classes and classes I knew of when I started writing the code and posts for the DataConnectors module, that included class- and unit-test stubs for ResultSet. As soon as the properties of IsResultSet were defined, the testPropertyCountAndTests test-method on ResultSet started failing - it now had properties that were visible through inspection, and no tests for those properties!

In order to allow the tests for the module to pass (for now), with an eye towards raising error once actual implementation in ResultSet was available, I added the following test-methods to the tests for ResultSet:

        # Test Properties

        def testCurrentRow( self ):
            """Unit-tests the CurrentRow property of the ResultSet class."""
            self.assertEquals( ResultSet.CurrentRow, IsResultSet.CurrentRow, 'Until CurrentRow is implemented in ResultSet, it should be inherited from IsResultSet.' )

        def testFieldNames( self ):
            """Unit-tests the FieldNames property of the ResultSet class."""
            self.assertEquals( ResultSet.FieldNames, IsResultSet.FieldNames, 'Until FieldNames is implemented in ResultSet, it should be inherited from IsResultSet.' )

        def testRowCount( self ):
            """Unit-tests the RowCount property of the ResultSet class."""
            self.assertEquals( ResultSet.RowCount, IsResultSet.RowCount, 'Until RowCount is implemented in ResultSet, it should be inherited from IsResultSet.' )

Since I know that there will be implementation in ResultSet, but it isn't there yet, I want these tests to pass until ResultSet has it's own implementation of those properties...

Basically, all I did here was to assert that the properties of the ResultSet class and IsResultSet interface were the same - so these will start breaking as soon as ResultSet has local implementation of those properties.

And I think that's enough for today. I'm not sure whether I'll go and implement TypedList next, or progress on to the nominal abstract classes of DataConnetors... I'll have to noodle on which makes the most sense.