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.

Monday, November 28, 2011

The DataConnectors module (part 1), and some ruminations on interfaces

Perhaps one of the reasons that I've been struggling with picking DBFSpy back up is that it has a lot of code in it that's only secondarily related to it's core purpose. That may sound kind of strange on the surface (I know I struggled with coming to the conclusion, and I'm not completely sure that it's correct, but for now, that's my story and I'm sticking to it).

Consider, for example, that ultimately, it should be something configurable (which I have accounted for with my last post), and that it has database-connectivity functionality. Neither of those is directly related to the FUSE-based-filesystem aspect of the project, though, to be fair, it couldn't do what it's intended to do without at least having the database-connection capabilities. That does not mean that the database-connectivity functionality needs to be a part of the core DBFSpy code, though. So, in the interests of both keeping the code modular, and of simplifying my future efforts on DBFSpy, I'm going to break the database-functionality out into it's own module: DataConnectors.

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

Let's start with some design first. By my reckoning, it needs to have the ability to connect to, query against, and return results from a back-end database. Since my database-focus (for now) is centered around MySQL, I'll stop when I get to a functional MySQL connection with querying capabilities, though I want to make sure that it's extensible enough to allow use with, say, ODBC data-sources, PostgreSQL, or whatever else is technically possible. Additionally, there needs to be some sort of common mechanism to allow objects to persist their state-data as needed, and to track that their state data has changed or has been created, and thus needs to be updated or created in the database. In my mind, then, the class-structure will look something like the class diagram below.

The Class Diagram

In today's post, I'm only going to cover one of the nominal interfaces of this module. There's a substantial amount of stuff going on even there, and I also wanted to share some of my thoughts (dare I call them insights?) on the creation of explicit interfaces.

In most languages that directly support explicit interfaces, those interface definitions do not allow the specification of anything other than abstract methods. They don't allow properties to be specified, nor do they allow any sort of true functionality to be defined for any methods - everything is an abstract method-stub.

That seems pretty straightforward.

But taking a step back, a question occurs to me. Let me lead into it with some other thoughts/questions, though. First off, what purposes are served by specifying an interface? One of them, certainly, is to provide a contract, right? When a class is defined that implements a particular interface, it's promising that it will provide the functionality specified by that interface. Somewhere along the line, whether as part of a compilation process, or at run-time, a failure of a class to make good on that promise should raise some kind of error (at run-time is, perhaps, less than optimal, but can be worked around with discipline, I believe). A secondary, but related purpose is to provide what I've been calling "type-identity" in my class' doc-strings. If a class implements an interface, and assuming that the contract associated with it is enforced in any fashion, the derivation from that interface allows our code to know that any given instance is of a given type.

My question, then, is "why not allow interfaces to specify properties?" Properties, even in a language like Python, where they have no set type, and can be defined on the fly, could just as easily be part of a contract, couldn't they?

The only arguments I've been able to find thus far to justify properties not being part of interface definitions all seem to be variations on "properties represent specific implementations of data representations, and would break encapsulation." I think I'd argue that, provided that properties didn't specify anything more than that they exist, and maybe what type they are (if applicable), that I'd disagree. But maybe I'm missing something.

The reason for most of the pondering above is that as I was thinking out some of the details for the interfaces in the DataConnectors module, it occurred to me that some of my nominal interfaces could easily specify/require that specific properties would at least exist for derived classes. It would even have been possible to force specific types, perhaps, though I suspect that it would've been more trouble than it would be worth (and may not have been possible without a lot of nominal interface clutter).

As a case in point, consider the IsDataConnector nominal interface.

IsDataConnector (Nominal interface):

class IsDataConnector( object ):
    """Provides interface definition and type-identity for objects that can represent connectors to a back-end data-source (typically a relational database engine)."""

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

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

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

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

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

    Connection = property( None, None, None, None )
    Database = property( None, None, None, None )
    Host = property( None, None, None, None )
    Password = property( None, None, None, None )
    User = property( None, None, None, None )

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

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

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

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def Connect( self ):
        """Connects to the database specified by Database, residing on Host, using User and Password to connect."""
        raise NotImplementedError( '%s.Connect is not implemented as required by IsDataConnector.' % ( self.__class__.__name__ ) )

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'query', None, 'The IsQuery object that will be executed against the object\'s Connection.' )
    def Query( self, query ):
        """Executes the provided IsQuery object's query against the object's connection, returning one or more results."""
        raise NotImplementedError( '%s.Query is not implemented as required by IsDataConnector.' % ( self.__class__.__name__ ) )

__all__ += [ 'IsDataConnector' ]

Commentary

Line(s)
24-28
Here is where I've defined the "required" properties for classes implementing the interface.
The practical upshot of these is that each property has been defined, but has no implementation associated with them (they have no getter, setter or deleter methods). The more I think about this approach, the more I like it. Consider:
  • Each property is now defined here, but has no implementation, and will raise errors when tested. In combination with the normal unit-testing processes (from previous posts) that requires a test-method for each property, this feels pretty safe.
  • That effectively requires (provided that discipline is maintained at the unit-testing level) that all derived classes will have to implement the properties defined here.
I also didn't specify a documentation-string for them, though I'm toying with the idea of putting the documentation string in at this level in the object/interface tree, which is why they aren't just property() calls. This also feels like another justification for explicitly defining properties using the property() function, rather than using the decorator-structure.
24
The Connection property will contain the actual database-connector object in a derived instance.
25
Database will contain the name of the database being connected to.
26
Host will contain the name or IP-address of the host machine that the Database-database resides on.
27
Password will contain the password used to connect to the database.
28
User will contain the user-name used to connect to the database.
49-52
Requires a common method/mechanism to connect to the database that the derived object represents a connection to.
55-60
Requires a common method/mechanism to execute a query against the database.

Looking at this definition, then, and applying it to BaseDataConnector and MySQLConnector (and any other connectors that may be derived from BaseDataConector), it should hopefully be obvious that they will have:

  • A Connection property;
  • A Database property;
  • A Host property;
  • A Password property;
  • A User property;
  • A Connect method;
  • A Query method; and
There are no implementation details, and it's not possible to determine (yet) where these properties and methods will actually be realized. I expect that the items that aren't specific to a given database-engine - most of the properties, possibly all of them, and the AppendToQueue method - will be implemented in BaseDataConenctor. That's the lowest common denominator for actual implementations of various database-connector objects, at least, so that would make the most sense, but we'll have to see how that works out later.

I'm going to stop here for today, while this post sinks in, but before I do, here are the unit-tests for the interface.

    class IsDataConnectorDerived( IsDataConnector ):
        def __init__( self ):
            pass
    
    class testIsDataConnector( unittest.TestCase ):
        """Unit-tests the IsDataConnector class."""
    
        def setUp( self ):
            pass
    
        def tearDown( self ):
            pass
        
        def testAbstract( self ):
            """Testing abstract nature of the IsDataConnector class."""
            try:
                testObject = IsDataConnector()
                self.fail( 'IsDataConnector is a nominal interface, and should not be instantiable.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'Attempting to instantiate an IsDataConnector should raise a NotImplementedError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            testObject = IsDataConnectorDerived()
            self.assertTrue( isinstance( testObject, IsDataConnector ), 'Objects derived from IsDataConnector should be instances of it.' )
    
        def testPropertyCountAndTests( self ):
            """Testing the properties of the IsDataConnector class."""
            items = getMemberNames( IsDataConnector )[0]
            actual = len( items )
            expected = 5
            self.assertEquals( expected, actual, 'IsDataConnector 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 IsDataConnector class."""
            items = getMemberNames( IsDataConnector )[1]
            actual = len( items )
            expected = 2
            self.assertEquals( expected, actual, 'IsDataConnector 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 ) )

        # Unit-test properties

        def testConnection( self ):
            """Unit-tests the Connection property of the IsDataConnector interface."""
            testObject = IsDataConnectorDerived()
            try:
                testObject.Connection = None
                self.fail( 'The Connection property of IsDataConnector is nominally abstract, and should not be settable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Trying to set the Connection property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                testValue = testObject.Connection
                self.fail( 'The Connection property of IsDataConnector is nominally abstract, and should not be gettable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Trying to get the Connection property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                del testObject.Connection
                self.fail( 'The Connection property of IsDataConnector is nominally abstract, and should not be deletable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Trying to delete the Connection property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testDatabase( self ):
            """Unit-tests the Database property of the IsDataConnector interface."""
            testObject = IsDataConnectorDerived()
            try:
                testObject.Database = None
                self.fail( 'The Database property of IsDataConnector is nominally abstract, and should not be settable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Trying to set the Database property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                testValue = testObject.Database
                self.fail( 'The Database property of IsDataConnector is nominally abstract, and should not be gettable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Trying to get the Database property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                del testObject.Database
                self.fail( 'The Database property of IsDataConnector is nominally abstract, and should not be deletable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Trying to delete the Database property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testHost( self ):
            """Unit-tests the Host property of the IsDataConnector interface."""
            testObject = IsDataConnectorDerived()
            try:
                testObject.Host = None
                self.fail( 'The Host property of IsDataConnector is nominally abstract, and should not be settable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Trying to set the Host property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                testValue = testObject.Host
                self.fail( 'The Host property of IsDataConnector is nominally abstract, and should not be gettable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Trying to get the Host property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                del testObject.Host
                self.fail( 'The Host property of IsDataConnector is nominally abstract, and should not be deletable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Trying to delete the Host property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testPassword( self ):
            """Unit-tests the Password property of the IsDataConnector interface."""
            testObject = IsDataConnectorDerived()
            try:
                testObject.Password = None
                self.fail( 'The Password property of IsDataConnector is nominally abstract, and should not be settable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Trying to set the Password property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                testValue = testObject.Password
                self.fail( 'The Password property of IsDataConnector is nominally abstract, and should not be gettable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Trying to get the Password property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                del testObject.Password
                self.fail( 'The Password property of IsDataConnector is nominally abstract, and should not be deletable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Trying to delete the Password property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testUser( self ):
            """Unit-tests the User property of the IsDataConnector interface."""
            testObject = IsDataConnectorDerived()
            try:
                testObject.User = None
                self.fail( 'The User property of IsDataConnector is nominally abstract, and should not be settable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Trying to set the User property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                testValue = testObject.User
                self.fail( 'The User property of IsDataConnector is nominally abstract, and should not be gettable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Trying to get the User property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                del testObject.User
                self.fail( 'The User property of IsDataConnector is nominally abstract, and should not be deletable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Trying to delete the User property of IsDataConnector should raise an Attribute error, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        # Unit-test methods

        def testConnect( self ):
            """Unit-tests the Connect method of the IsDataConnector interface."""
            testObject = IsDataConnectorDerived()
            try:
                testObject.Connect()
                self.fail( 'Connect should raise a NotImplementedError.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'Connect should raise a NotImplementedError, but %s was raised instead:\n  %s' % ( error.__class__.__mame__, error ) )

        def testQuery( self ):
            """Unit-tests the Query method of the IsDataConnector interface."""
            testObject = IsDataConnectorDerived()
            try:
                testObject.Query( None )
                self.fail( 'Query should raise a NotImplementedError.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'Query should raise a NotImplementedError, but %s was raised instead:\n  %s' % ( error.__class__.__mame__, error ) )

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

Friday, November 25, 2011

Not posting today...

It's Thanksgiving here in the US, so I'm taking the day off.

Come back on Monday for the next installment, when I'll start looking at a data-connectors module.

Wednesday, November 23, 2011

The Configuration module

One of the requirements for the project I mentioned that spawned the idea of the MiniAppServer project was the need to be "configurable," at least for some value of "configurable." In pondering what I've posted over the past couple of days for MiniAppServer, it occurred to me that the original "configurable" requirement would, logically, carry through here as well, and that I hadn't addressed it at all thus far.

And, whether I proceed with DBFSpy, restart it, or shelve it in favor of MiniAppServer for the time being, I'll likely want configuration capabilities.

So. Python's got at least one configuration-related module in it's default installation: ConfigParser. It's aimed at providing configuration-capabilities based on what I think of as a typical configuration-file format (I'm pretty sure I've seen it in Windows and Linux environments over the years, at least), that looks something like this:

#########################################################
# "#" indicates the beginning of a comment that         #
# runs to the end of the line                           #
#########################################################
[Section Name]                # Configuration-section name. 
                              # Spaces are allowed!
keyname:            value     # keynames can be mixed-case, 
                              # but come back as lower-case
keyname2:           value2    # values are returned as 
                              # strings by default.

Since the configuration types, section-names, values, etc., will all vary significantly from one configurable type to another, there's not a whole lot that needs to be coded: An interface and an abstract class, to my thinking, cover most of it, since the specific implementations will have to dig out and format and/or convert the sections, key-names and values. The classes and their unit-tests are pretty simple as a result.

The complete code is shared at dl.dropbox.com/u/1917253/site-packages/Template.py

Stripping the items in question down to just the classes and their unit-tests, I get:

IsConfigurable (Nominal interface):

class IsConfigurable( object ):
    """Provides interface requirements and type-identoty for objects that can retrieve 
state-data from a configuration file (at present, only ConfigParser is supported)."""

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

    ConfigurationTypes = [ ConfigParser.ConfigParser ]

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

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

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

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

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

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

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

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'configProvider', None, 'A configuration provider object, an instance of one of IsConfigurable.ConfigurationTypes' )
    @DocumentArgument( 'argument', 'configSection', None, 'String, required) The configuration-section name that the object\'s configuration-state should be retrieved from.' )
    @DocumentConfiguration( 'Section Name', 'keyname', 'value' )
    @DocumentConfiguration( 'Section Name', 'keyname2', 'value2' )
    def Configure( self, configProvider, configSection ):
        """Configures the object using configuration data from the specified section of the specified provider."""
        raise NotImplementedError( '%s.Configure has not been implemented' % ( self.__class__.__name__ ) )

__all__ += [ 'IsConfigurable' ]

Note the use of the @DocumentConfiguration decorator on lines 49 and 50. Documentation of this sort of configuration information is what that decorator was designed for, and will provide documentation that looks like this:

Configure(self, configProvider, configSection)
    Configures the object using configuration data from the specified 
    section of the specified provider.
     
    ###################
    ## Arguments     ##
    ###################
     + self ................ The object-instance that the method is called against. 
     + configProvider ...... A configuration provider object, an instance of one of 
                             IsConfigurable.ConfigurationTypes 
     + configSection ....... String, required) The configuration-section name that 
                             the object's configuration-state should be retrieved 
                             from. 
     
    ###################
    ## Configuration ##
    ###################
     
    The relevant configuration-file section(s) and values(s) are:
    [Section Name]
    keyname ..................... value 
    keyname2 .................... value2

The relevant unit-tests:

    class IsConfigurableDerived( IsConfigurable ):
        def __init__( self ):
            IsConfigurable.__init__( self )
    
    class testIsConfigurable( unittest.TestCase ):
        """Unit-tests the IsConfigurable class."""
    
        def setUp( self ):
            pass
    
        def tearDown( self ):
            pass
        
        def testDerived( self ):
            """Testing abstract nature of the IsConfigurable class."""
            try:
                testObject = IsConfigurable()
                self.fail( 'IsConfigurable is nominally an interface, and should not be instantiable.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'Instantiating IsConfigurable should raise a NotImplementedError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            testObject = IsConfigurableDerived()
            self.assertTrue( isinstance( testObject, IsConfigurable ), 'Objects implementing IsConfigurable should be instances of IsConfigurable.' )
    
        def testPropertyCountAndTests( self ):
            """Testing the properties of the IsConfigurable class."""
            items = getMemberNames( IsConfigurable )[0]
            actual = len( items )
            expected = 0
            self.assertEquals( expected, actual, 'IsConfigurable 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 IsConfigurable class."""
            items = getMemberNames( IsConfigurable )[1]
            actual = len( items )
            expected = 1
            self.assertEquals( expected, actual, 'IsConfigurable 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 ) )

        # Unit-test properties

        # Unit-test methods

        def testConfigure( self ):
            """Unit-tests the Configure method of the IsConfigurable class."""
            testObject = IsConfigurableDerived()
            try:
                testObject.Configure( ConfigParser.ConfigParser(), 'configSection' )
                self.fail( 'An IsConfigurable instance that has not overridden Configure should raise a NotImplementedError when it is called.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'An IsConfigurable instance that has not overridden Configure should raise a NotImplementedError when it is called, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

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

HasConfiguration (Nominal abstract class):

class HasConfiguration( object ):
    """Provides a configuration property (allowing an instance of any of the types in 
IsConfigurable.ConfigurationTypes)."""

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

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _GetConfiguration( self ):
        """Gets the object's associated configuration object."""
        return self._configuration

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'value', None, '(Any of %s, required) The configuration-provider object that will contain the object\'s configuration-state data.' % ( IsConfigurable.ConfigurationTypes ) )
    def _SetConfiguration( self, value ):
        """Sets the object's associated configuration object."""
        if not value.__class__ in IsConfigurable.ConfigurationTypes:
            raise TypeError( '%s.Configuration expects an instance of one of %s' % ( self.__class__.__name__, IsConfigurable.ConfigurationTypes ) )
        self._configuration = value

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _DelConfiguration( self ):
        """Deletes the object's associated configuration object."""
        self._configuration = None

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

    Configuration = property ( _GetConfiguration, None, None, 'Gets the object\'s associated configuration object.' )

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @ToDo( 'Document class.' )
    def __init__( self ):
        """Object constructor."""
        # Nominally abstract: Don't allow instantiation of the class
        if self.__class__ == HasConfiguration:
            raise NotImplementedError( 'HasConfiguration is (nominally) an abstract class, and is not intended to be instantiated.' )
        # Set default property value(s)
        self._DelConfiguration()

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

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

__all__ += [ 'HasConfiguration' ]

The relevant unit-tests:

    class HasConfigurationDerived( HasConfiguration ):
        def __init__( self ):
            HasConfiguration.__init__( self )
    
    class testHasConfiguration( unittest.TestCase ):
        """Unit-tests the HasConfiguration class."""
    
        def setUp( self ):
            pass
    
        def tearDown( self ):
            pass
        
        def testDerived( self ):
            """Testing abstract nature of the HasConfiguration class."""
            try:
                testObject = HasConfiguration()
                self.fail( 'HasConfiguration is nominally an abstract class, and should not be instantiable.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'Instantiating HasConfiguration should raise a NotImplementedError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            testObject = HasConfigurationDerived()
            self.assertTrue( isinstance( testObject, HasConfiguration ), 'Objects implementing HasConfiguration should be instances of HasConfiguration.' )
            self.assertEquals( testObject.Configuration, None, 'A default instance of HasConfiguration should have a None value for it\'s Configuration property.' )
    
        def testConstruction( self ):
            """Testing construction of the HasConfiguration class."""
            pass
    
        def testPropertyCountAndTests( self ):
            """Testing the properties of the HasConfiguration class."""
            items = getMemberNames( HasConfiguration )[0]
            actual = len( items )
            expected = 1
            self.assertEquals( expected, actual, 'HasConfiguration 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 HasConfiguration class."""
            items = getMemberNames( HasConfiguration )[1]
            actual = len( items )
            expected = 0
            self.assertEquals( expected, actual, 'HasConfiguration 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 ) )

        # Unit-test properties

        def testConfiguration( self ):
            """Unit-tests the Configuration property of the HasConfiguration class."""
            testObject = HasConfigurationDerived()
            goodValues = IsConfigurable.ConfigurationTypes
            badValues = [ None, 'string', 1, 1L, 1.0, True ]
            for testClass in goodValues:
                testValue = (testClass)()
                testObject._SetConfiguration( testValue )
                self.assertEquals( testObject.Configuration, testValue, 'The property set (%s) should be retrievable by a get.' % ( testValue ) )
            for testValue in badValues:
                try:
                    testObject._SetConfiguration( testValue )
                    self.fail( 'Setting the configuration property to an item that is not an instance of %s (%s) should raise a TypeError' % ( IsConfigurable.ConfigurationTypes, testValue ) )
                except TypeError:
                    pass
                except Exception, error:
                    self.fail( 'Setting the configuration property to an item that is not an instance of %s (%s) should raise a TypeError, but %s was raised instead:\n  %s' % ( IsConfigurable.ConfigurationTypes, testValue, error.__class__.__name__, error ) )

        # Unit-test methods

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

Monday, November 21, 2011

Design for the MiniAppServer idea

The design for MiniAppServer feels like it ought to be fairly simple, given what I'm thinking is a typical development-case:
  • Create a MiniAppServer instance, with various properties, including:
    • Host name;
    • Port;
    • A document-root, if desired;
    • A top-level request-routing mechanism (something that can handle application-ish endpoints and/or static-file requests/responses);
    • Any application-ish endpoint-handlers needed;
    • Any error-handling endpoint-handlers needed;
  • If necessary, add/register any non-standard endpoint-handlers;
  • If necessary, add/register any standard handlers for errors, etc.;
  • Call MiniAppServer.serve_forever()
When a request is recieved by the MiniAppServer instance, it gets handled as follows:
  1. The request (plus it's socket and whatever other information/objects are needed by a BaseRequestHandler instance) are passed to the application's top-level routing object.
  2. That routing object examines the request made, looking for a match based on:
    1. Registered application-ish endpoints;
    2. Static-file endpoints;
    3. Other endpoints? I can't think of any that make sense, but I can't rule out the possibility...
    If a match is found, the request (and everything associated) is passed to a new instance of the appropriate endpoint class.
  3. The endpoint-handler does whatever it needs to do, generating whatever output it needs to generate, and returns that response.
    • If an error is encountered, then the request (etc.) should be passed to the registered error-handling endpoint-handler, if there is one;
    • If there is not a specific error-handling endpoint, a simple, raw response should be returned with the appropriate HTTP code);
Given all of this, what I'm expecting (at least as of this writing) is an object-structure something like so:
IsApplication (nominal interface)
Provides interface requirements and type-identioty for objects that can provide MiniAppServer-like functionality. This is specifically called out becuse there would otherwise be circular references between MiniAppServer and HasApplication.
HasApplication (nominal abstract class)
Provides an Application property for derived objects, which should be an instance implementing IsApplication.
IsEndpoint (nominal abstract class)
Provides baseline functionality, interface requirements and type-identity for objects that can act as endpoints for a MiniAppServer instance. Endpoint definitions should be derived from this and SocketServer.BaseRequestHandler, though I'm not sure whether I want to make IsEndpoint a subclass of SocketServer.BaseRequestHandler directly, or just have it check to make sure that instances derived from it are also derived from SocketServer.BaseRequestHandler.
BaseRouter (nominal abstract class), or
Router (class, extensible)
Ultimately, I'm expecting that a running MiniAppServer instance will require a router-object: something that the recieved requests are handed off to, that then spins off an appropriate IsEndpoint-derived instance to handle the request and return results to the client. Whether this should be a class in and of itself, or a nominal abstract class that developers should derive their own classes from, I'm not sure yet. There are potential advantages to both, and I'm very much on the fence as to which way to go with it.
ErrorEndpoint (class, extensible, derives from IsEndpoint and HasApplication)
An IsEndpoint-derived class that will be handed control over an incoming request when/if an error occurs.
FileEndpoint (class, final, derives from IsEndpoint and HasApplication)
An IsEndpoint-derived class that will handle looking up static files for responses to requests that don't get caught by a functional IsEndpoint instance first.
MiniAppServer (class, extensible, implements IsApplication)
The class that's actually responsible for generating and starting a server instance.
Working my way from the top of this list down seems a rational approach, at least for now...

Friday, November 18, 2011

Requirements for the MiniAppServer idea

So, while I'm trying to re-familiarize myself with the DBFSpy stack, I'm gonna capture some of the requirements ideas for the free-standing, Python-based mini-web-app-server idea that I mentioned in my last post. That entire string (free-standing, Python-based mini-web-app-server) is a bit much to type over and over again, it doesn't lend itself well to a meaningful acronym or abbreviation, and all of the logical names I can think of are already taken (Servelets, Applets, etc.), so I'm going to call it MiniAppServer, at least for now...

I mentioned that the idea came to me while I was working on a Python project at work. Without going into too much detail on that project (since I'm not sure if I can, legally), it basically boiled down to a free-standing Python-based service that would periodically query information from a remote logging service, potentially perform some sort of examination of the data returned, and respond if certain criteria were met (sending an email if our 404-count exceeded some threshold, for example).

One of the requirements for that project was that it be "configurable." There wasn't much in the way of definition for what "configurable" meant, so I started with the default of using the ConfigParser module's offerings to retrieve configuration information from a fairly typical configuration-file. While on the bus back home one night, it suddently occurred to me that if configuration-changes needed to be made, that would entail stopping the service, changing the configuration, and re-starting it. Admittedly, it might not happen in that order; given the shape of the code I came up with, altering the config-file, then restarting the service would have worked as well. But that started me thinking "what if there were a way to have the service also generate a web-based configuration-interface. Something that would allow IT staff to simply hit some URL and make changes to the configured values in real-time.

That was the impetus for MiniAppServer.

Whether the original idea for it's use becomes valid or not (the project at work was effectively cancelled, and I mothballed all of the code for it, following my previous recommendations as much as I could), I can certainly come up with some uses for such a thing for my own ends, so I'm adding it to my stack of projects.

So, what are MiniAppServer's requirements?
  • It needs to be able to run on any machine (with a full Python installation being the only requirement);
  • URLs should be (for anyting that isn't a static-file request/response) something along the lines of http://host:port/ObjectType/Action[/Format], where:
    ObjectType
    is the name of an object-type (perhaps a class, perhaps not)
    Action
    is some sort of action to be taken with the object-type. Some likely candidates include:
    • List - show a list of available items of the applicable object-type, probably as links to a View for the individual items;
    • View - Show a detailed view of a specific instance of an object-type;
    • Edit or Update - make and submit changes to one instance of a given object-type;
    • Create or New - create a new instance of an object-type (which may or may not be substantially different that the Edit form/view);
    • Delete - Delete a single instance of an object-type
    Format
    is (maybe) a format to return - XML, JSON, or whatever else. I cannot come up with a real use for this that justifies it to my satisfaction, and perhaps allowing a query-string (e.g., ?format=xml) instead would be better. I'll noodle on this for a while, I think;
    Some examples might include (for a User object-type):
    • http://localhost:8192/User/List
    • http://localhost:8192/User/View (this would need some sort of ID to be passed, maybe as an additional path element [.../View/USERID] or as a query-string)
    • http://localhost:8192/User/Edit (same comment as above)
    • http://localhost:8192/User/Create
    • http://localhost:8192/User/Delete
  • It has to handle GET and POST requests, in order to support complete HTML form-interaction;
    • It needs to be able to handle "simple" POST submissions (e.g., text-only);
    • It will likely need to be able to handle binary upload-capable POST submissions as well, though that may not be an immediate requirement.
  • It needs to be as small as possible - there's no point in having a whole lot of overhead for an application-server of this sort when a LAMP stack could just as easily be built with mod-python;
  • At the same time, it needs to be highly extensible;
  • It must be able to serve static files as well (for things like images, stylesheets and JavaScript);
  • It should be something that could be set up to run as a service on a machine, or used as a local instance of an application.
Some "nice-to-have" features include:
  • It should support as much of a Model-view-controller architecture/structure as is feasible;
  • It would be neat if the entire application-site (if there are any non-Python files associated, at least) could be packaged up as a single zip-file or tarball;

Wednesday, November 16, 2011

How to mothball a project

Part of the reason that the last several posts I've made since I returned from my hiatus have not been obviously related to the DBFSpy project that I started this blog with is that in the intervening couple of months, I've forgotten a lot of what I'd intended to do with it.

Fortunately, I had documented at least a sequence of classes, interfaces, etc., that I'd expected to cover, so it's not like I'm completely lost. At the same time, though, I hadn't gotten even as far as stubbing out properties and methods for the remaining items before things got out of control, and I had no time to continue posting.

When projects are in danger of needing to be mothballed, I like to try and spend as much time as I can get away with making sure that I capture as much of my thought-process as I can before I have to set it aside. I don't always get as much of this kind of time as I'd like, though, even when the project in question is my own.

Case in point: All I really have on DBFSpy is a list of classes and interfaces I was expecting, and some indication that they were nominally complete (though maybe not with full unit-tests, etc.):
  • BaseDatabaseConnection (abstract class, complete)
  • BaseDataObject (abstract class, complete)
  • BaseFileSystemItemCache (abstract class, complete)
  • IFilesystem (interface, complete)
  • IFilesystemItem (interface)
  • IFileystemOperators (interface)
  • MySQLDatasource (class)
  • MySQLFileItem (class)
  • MySQLFilesystem (class)
  • Unit-tests for classes, etc.
  • dbfspy (executable)
Looking at older posts here, it looks like I was about on track with this list. I also managed to create and post the UML diagram, which I hope will refresh my memory somewhat as I start digging into that project again. Though now I suspect I'll need to review my progress (it looks like I had to break off after IFilesystem, bu before IFilesystemItem). With some luck, given my new insights into testing, and the discovery of properties and methods that it facilitates, I'll be able to recapture a lot of my previous momentum, and bang things out quickly. If not, I guess I'll have to think about either setting it aside again (with the expectation that it'll be so full of holes by the time I pick it up again that I'll be starting from scratch), or just biting that bullet now, and re-starting.

So, chalk this up to another lesson learned - take the time to document and comment as you go. You never know when you may have to set a project aside, and if there isn't time to gracefully (and thoroughly) mothball it, it may very well be full of holes by the time you get back to it.

What I think I'd advocate is (at the very least):
  • Make sure all decisions are documented;
  • If possible, make sure that any class-diagrams are complete as of the time the project had to be stopped;
  • Make sure that the design is thorough, documented, and annotated as needed to keep track of what the design intended to achieve, and what modifications had to be made, if any.
There may be other items as well, but these are, I hope, the major items...

Another part of the reason I spent the last few posts going over new stuff (apart from my enthusiasm for the topic therein), was that as I was working through a Python project at work, it occurred to me that it might be a cool idea to generate a small, freestanding, Python-based web-application server framework. Nothing huge, you understand, but rather something that could be incorporated into other Python projects, to allow for things like administrative interfaces for services and the like.

More on that topic later, I think...

Monday, November 14, 2011

The TypedContainers Module

Code-sharing: dl.dropbox.com/u/1917253/site-packages/TypedContainers.py

One of the other items I came up with during my month-or-so-long hiatus from blogging about My Brain on Python was spurred by a perceived need to have some container objects that were, for lack of a better term, strongly typed as far as it's values were concerned. Python (relying on duck-typing, as previously noted) doesn't provide such things out of the box, but since it's a very capable dynamic language, it proved to be fairly simple to implement these.

My initial item in this realm was a strongly-typed Dictionary equivalent - something that would allow a developer to define a dictionary-like data-structure that would allow only items that were of a give type or one of a group of types to be added to it. It's certainly possible to perform that kind of type-checking through a class-method before adding an item to a dictionary property, but if the dictionary itself is publicly accessible, there's no good way that I can see to prevent code from being written that would skip any such type-checking, and, for example, add a string member to a dictionary that was intended to contain only numbers.

A Python purist might very well (and with some justification, perhaps) say that this isn't really an issue - in the case of an error arising at runtime, an exception would be raised, and the maintainer of the code would have to track that down and fix it. I maintain that, while that is certainly true enough, it'd be easier to track the source of the error down if the simple act of trying to insert the errant value into the container caused the error, rather than waiting until it caused some other issue later in the execution of the code. Maybe a lot later.

So, a TypedDictionary. At first, it was free-standing, but as I thought through the potential for needing similar strongly-typed containers based off of lists or tuples, or whatever else might arise, I decided to abstract some of the functionality out into a nominal abstract class. The results (minus the stub-code for TypedList and TypedTuple, which I haven't dug into yet) look like this:

# TypedCollections.py
"""Provides strongly-typed extensions to Python collection-types (lists, dictionaries, tuples, etc.)."""

__author__  = 'Brian D. Allbee'
__licence__ = 'http://creativecommons.org/licenses/by-sa/3.0/'
__version__ = '0.2'
__copyright__ = "Copyright 2011, Brian D. Allbee"
__credits__ = ["Brian D. Allbee"]
__maintainer__ = "Brian D. Allbee"
__email__ = "brian.allbee@gmail.com"
__status__ = "Development"

#####################################
# Build an "__all__" list to        #
# support                           #
# "from Namespace.blah import *"    #
# syntax                            #
#####################################
__all__ = []

#####################################
# Required imports                  #
#####################################

from DocMeta import *
import types

#####################################
# Constants defined in the module   #
#####################################

AllTypes = []
for typeName in dir(types):
    if not typeName in [ 'ClassType', 'TypeType', 'InstanceType', 'ObjectType' ] and not typeName[0] == '_':
        AllTypes.append( eval( 'types.%s' % ( typeName ) ) )

__all__ += [ 'AllTypes' ]

#####################################
# Functions defined in the module   #
#####################################

#####################################
# Interfaces defined in the module  #
#####################################

#####################################
# Abstract Classes defined in the   #
# module                            #
#####################################

class BaseTypedCollection( object ):
    """Provides baseline functionality, interface requirements, and type-identity for derived items that can have a MemberTypes property."""

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

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _GetMemberTypes( self ):
        """Gets the set of object-types allowed as members in the collection."""
        return self._memberTypes

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'value', None, '(Iterable, required) An iterable collection of items to replace the current member-types with' )
    def _SetMemberTypes( self, value ):
        """Sets the set of object-types allowed as members in the collection."""
        self._memberTypes = []
        for item in value:
            # Basic types, excluding ClassType, TypeType, etc.
            if item in AllTypes:
                self._memberTypes.append( item )
            # Classic classes
            elif type( item ) == types.ClassType:
                raise ValueError( '%s.MemberTypes cannot accept classic classes (%s) as member-types' % ( self.__class__.__name__, item ) )
            # New-style classes
            elif type( item ) == types.TypeType:
                self._memberTypes.append( item )
            # Unrecognized!
            else:
                raise ValueError( '%s.MemberTypes cannot accept %s as a member-type' % ( self.__class__.__name__, item ) )

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    def _DelMemberTypes( self ):
        """Deletes the set of object-types allowed as members in the collection."""
        self._memberTypes = []

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

    MemberTypes = property( _GetMemberTypes, None, None, 'Gets the set of object-types allowed as members in the collection.' )

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

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

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

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'value', None, 'The value to check.' )
    def IsValidMemberType( self, value ):
        """Determines whether the supplied value is a valid type to be allowed in the collection-object."""
        if hasattr( value.__class__, '__mro__' ):
            compareTypes = set( value.__class__.__mro__ )
            return ( len( set( self._memberTypes ).intersection( compareTypes ) ) > 0 )
        if hasattr( value.__class__, '__bases__' ):
            compareTypes = set( value.__class__.__bases__ )
            return ( len( set( self._memberTypes ).intersection( compareTypes ) ) > 0 )
        raise TypeError( '%s.IsValidMemberType could not determine either an MRO for, or the bases of the %s value' % ( self.__class__.__name__, value ) )

__all__ += [ 'BaseTypedCollection' ]

#####################################
# Classes defined in the module     #
#####################################

class TypedDictionary( BaseTypedCollection, dict ):
    """Represents a strongly-typed Dictionary."""

    #################################
    # 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 dictionary.' )
    def __init__( self, memberTypes ):
        """Object constructor."""
        # Nominally final: Don't allow any class other than this one
        if self.__class__ != TypedDictionary:
            raise NotImplementedError( 'TypedDictionary is (nominally) a final class, and is not intended to be derived from.' )
        BaseTypedCollection.__init__( self )
        self._SetMemberTypes( memberTypes )

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

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

    @DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
    @DocumentArgument( 'argument', 'name', None, 'The key-name for the element once it\'s added to the dictionary.' )
    @DocumentArgument( 'argument', 'value', None, 'The element-name to add to the dictionary.' )
    def __setitem__( self, name, value ):
        """Wraps the default __setitem__ in order to type-check the value before allowing it to be added."""
        if not self.IsValidMemberType( value ):
            raise TypeError( 'TypedDictionary instance expects an instance of any of %s.' % ( self._memberTypes ) )
        dict.__setitem__( self, name, value )

__all__ += [ 'TypedDictionary' ]

#####################################
# Package sub-modules and -packages #
#####################################

#####################################
# Unit-test the module on main      #
#####################################

if __name__ == '__main__':
    import inspect, os, sys, unittest
    from UnitTestUtilities import *

    testSuite = unittest.TestSuite()
    testResults = unittest.TestResult()

    ##################################
    # Unit-test Constants            #
    ##################################

    class testConstants( unittest.TestCase ):
        """Unit-tests the constants defined in the module."""
        pass
    
    testSuite.addTests( unittest.TestLoader().loadTestsFromTestCase( testConstants ) )

    ##################################
    # Unit-test Functions            #
    ##################################

    ##################################
    # Unit-test Interfaces           #
    ##################################

    ##################################
    # Unit-test Abstract Classes     #
    ##################################

    class BaseTypedCollectionDerived( BaseTypedCollection ):
        def __init__( self ):
            BaseTypedCollection.__init__( self )

    class AClassicMember: pass

    class AnotherClassicMember: pass

    class ADerivedClassicMember( AClassicMember ): pass

    class ATypedMember( object ): pass

    class AnotherTypedMember( object ): pass
    
    class ADerivedTypedMember( ATypedMember, object ): pass

    class testBaseTypedCollection( unittest.TestCase ):
        """Unit-tests the BaseTypedCollection class."""
    
        def setUp( self ):
            pass
    
        def tearDown( self ):
            pass
        
        def testAbstract( self ):
            """Testing abstract nature of the BaseTypedCollection class."""
            try:
                testObject = BaseTypedCollection()
                self.fail( 'BaseTypedCollection is nominally an abstract class and should not allow instantiation.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'Attempting to instantiate a BaseTypedCollection should raise a NotImplementedError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )

        def testConstruction( self ):
            """Testing construction of the BaseTypedCollection class."""
            # Single type
            testObject = BaseTypedCollectionDerived()
            self.assertTrue( isinstance( testObject, BaseTypedCollection ), 'Constructed BaseTypedCollection-derived objects should be instances of BaseTypedCollection.' )

        def testSimpleTypes( self ):
            """Testing the use of simple value-types as member-types."""
            testTypesList = [
                [ types.StringType, ],
                [ types.IntType, types.FloatType, types.LongType ],
                ]
            for testTypes in testTypesList:
                testObject = BaseTypedCollectionDerived()
                testObject._SetMemberTypes( testTypes )
                self.assertEquals( testObject.MemberTypes, testTypes, 'Getting member-types %s should return the types set %s' % ( testObject.MemberTypes, testTypes ) )

        def testSimpleInstances( self ):
            """Testing the use of simple value-types as member-types."""
            testTypesList = [
                [ 'ook', ],
                [ 1, 1L, 1.0 ],
                ]
            for testTypes in testTypesList:
                testObject = BaseTypedCollectionDerived()
                try:
                    testObject._SetMemberTypes( testTypes )
                    self.fail( 'Simple values %s should not be allowed as member-types' % ( testTypes ) )
                except ValueError:
                    pass
                except Exception, error:
                    self.fail( 'Attempting to use %s should raise a ValueError, but %s was raised instead:\n  %s' % ( testTypes, error.__class__.__name__, error ) )

        def testClassicClassTypes( self ):
            """Testing the use of classic classes as member-types."""
            testTypesList = [
                [ AClassicMember, ],
                [ AClassicMember, AnotherClassicMember ],
                ]
            for testTypes in testTypesList:
                testObject = BaseTypedCollectionDerived()
                try:
                    testObject._SetMemberTypes( testTypes )
                    self.fail( 'Classic classes %s should not be allowed as member-types' % ( testTypes ) )
                except ValueError:
                    pass
                except Exception, error:
                    self.fail( 'Attempting to use %s should raise a ValueError, but %s was raised instead:\n  %s' % ( testTypes, error.__class__.__name__, error ) )

        def testClassicClassInstances( self ):
            """Testing the use of classic classes as member-types."""
            testTypesList = [
                [ AClassicMember(), ],
                [ AClassicMember(), AnotherClassicMember() ],
                ]
            for testTypes in testTypesList:
                testObject = BaseTypedCollectionDerived()
                try:
                    testObject._SetMemberTypes( testTypes )
                    self.fail( 'Simple values %s should not be allowed as member-types' % ( testTypes ) )
                except ValueError:
                    pass
                except Exception, error:
                    self.fail( 'Attempting to use %s should raise a ValueError, but %s was raised instead:\n  %s' % ( testTypes, error.__class__.__name__, error ) )

        def testNewClassTypes( self ):
            """Testing the use of classic classes as member-types."""
            testTypesList = [
                [ ATypedMember, ],
                [ ATypedMember, AnotherTypedMember ],
                ]
            for testTypes in testTypesList:
                testObject = BaseTypedCollectionDerived()
                testObject._SetMemberTypes( testTypes )
                self.assertEquals( testObject.MemberTypes, testTypes, 'Getting member-types %s should return the types set %s' % ( testObject.MemberTypes, testTypes ) )

        def testNewClassInstances( self ):
            """Testing the use of classic classes as member-types."""
            testTypesList = [
                [ ATypedMember(), ],
                [ ATypedMember(), AnotherTypedMember() ],
                ]
            for testTypes in testTypesList:
                testObject = BaseTypedCollectionDerived()
                try:
                    testObject._SetMemberTypes( testTypes )
                    self.fail( 'Instance-values %s should not be allowed as member-types' % ( testTypes ) )
                except ValueError:
                    pass
                except Exception, error:
                    self.fail( 'Attempting to use %s should raise a ValueError, but %s was raised instead:\n  %s' % ( testTypes, error.__class__.__name__, error ) )

        def testProperties( self ):
            """Testing the properties of the BaseTypedCollection class."""
            testObject = BaseTypedCollectionDerived()
            # MemberTypes (get)
            self.assertEquals( testObject.MemberTypes, [], 'The default MemberTypes value should be an empty list.' )
            try:
                testObject.MemberTypes = None
                self.fail( 'The abstract property MemberTypes should not be settable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Attempting to set the MemberTypes property should raise a AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
            try:
                del testObject.MemberTypes
                self.fail( 'The abstract property MemberTypes should not be deletable' )
            except AttributeError:
                pass
            except Exception, error:
                self.fail( 'Attempting to delete the MemberTypes property should raise a AttributeError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
    
        def testMethods( self ):
            """Testing the methods of the BaseTypedCollection class."""
            testObject = BaseTypedCollectionDerived()
            # IsValidMemberType
            # - Simple type
            testObject._SetMemberTypes( [ int ] )
            goodValues = [ -1, 0, 1, 2 ]
            for value in goodValues:
                self.assertTrue( testObject.IsValidMemberType( value ), 'The value %s should be a good value.' % ( value ) )
            badValues = [ None, 'ook', 1L, 1.0 ]
            for value in badValues:
                self.assertFalse( testObject.IsValidMemberType( value ), 'The value %s should be a bad/invalid value.' % ( value ) )
            # - Multiple simple types
            testObject._SetMemberTypes( [ int, float, long ] )
            goodValues = [ -1, 0, 1, 2, -1.0, 0.0, 1.0, 2.0, -1L, 0L, 1L, 2L ]
            for value in goodValues:
                self.assertTrue( testObject.IsValidMemberType( value ), 'The value %s (%s) should be a good value.' % ( value, type( value ) ) )
            badValues = [ None, 'ook', list(), dict(), tuple() ]
            for value in badValues:
                self.assertFalse( testObject.IsValidMemberType( value ), 'The value %s (%s) should be a bad/invalid value.' % ( value, type( value ) ) )
            # - New-style class type
            testObject._SetMemberTypes( [ ATypedMember ] )
            goodValues = [ ATypedMember(), ADerivedTypedMember() ]
            for value in goodValues:
                self.assertTrue( testObject.IsValidMemberType( value ), 'The value %s (%s) should be a good value.' % ( value, type( value ) ) )
            badValues = [ None, 'ook', 1, list(), tuple(), dict(), AClassicMember(), AnotherTypedMember() ]
            for value in badValues:
                self.assertFalse( testObject.IsValidMemberType( value ), 'The value %s (%s) should be a bad/invalid value.' % ( value, type( value ) ) )
            # - Multiple new-style class type
            testObject._SetMemberTypes( [ ATypedMember, AnotherTypedMember ] )
            goodValues = [ ATypedMember(), ADerivedTypedMember(), AnotherTypedMember() ]
            for value in goodValues:
                self.assertTrue( testObject.IsValidMemberType( value ), 'The value %s (%s) should be a good value.' % ( value, type( value ) ) )

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

    ##################################
    # Unit-test Classes              #
    ##################################

    class TypedDictionaryDerived( TypedDictionary ):
        def __init__( self, memberTypes ):
            TypedDictionary.__init__( self, memberTypes )
    
    class testTypedDictionary( unittest.TestCase ):
        """Unit-tests the TypedDictionary class."""
    
        def setUp( self ):
            pass
    
        def tearDown( self ):
            pass
        
        def testFinal( self ):
            """Testing final nature of the TypedDictionary class."""
            try:
                testObject = TypedDictionaryDerived( [ types.StringType, ] )
                self.fail( 'TypedDictionary is nominally a final class and should not allow derivation.' )
            except NotImplementedError:
                pass
            except Exception, error:
                self.fail( 'Attempting to derive from TypedDictionary should raise a NotImplementedError, but %s was raised instead:\n  %s' % ( error.__class__.__name__, error ) )
    
        def testConstruction( self ):
            """Testing construction of the TypedDictionary class."""
            testObject = TypedDictionary( [ int, long ] )
            self.assertTrue( isinstance( testObject, TypedDictionary ), 'Tautological imperative: A TypedDictionary is a TypedDictionary...!' )
    
        def testDictionary( self ):
            """Testing the dictionary capabilities of the TypedDictionary class."""
            # PropertyName
            testObject = TypedDictionary( [ int, long ] )
            goodNames = [ 'ook', '1', 1, '', None, TypedDictionaryDerived ]
            goodValues = [ -1, 0, 1, 2, -1L, 0L, 1L, 2L ]
            for name in goodNames:
                for value in goodValues:
                    testObject[ name ] = value
                    self.assertEquals( testObject[ name ], value, 'The value set (%s) should be returned at the name (%s)' % ( value, name ) )
            badValues = [ None, -1.0, 0.0, 1.0, 2.0, 'ook' ]
            for name in goodNames:
                for value in badValues:
                    try:
                        testObject[ name ] = value
                        self.fail( 'The value %s should not be allowed in the dictionary' % ( value ) )
                    except TypeError:
                        pass
                    except Exception, error:
                        self.fail( 'Setting an invalid value (%s) in a TypedDictionary should raise a TypeError, but %s was raised.\n  %s' % ( value, error.__class__.__name__, error ) )
    
        def testProperties( self ):
            """Testing the properties of the TypedDictionary class."""
            # No local properties to test (see BaseTypedCollection)
            pass
    
        def testMethods( self ):
            """Testing the methods of the TypedDictionary class."""
            # No local methods to test (see BaseTypedCollection)
            pass

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

    # Run unit-tests and install
    print '#'*79
    print 'Running unit-tests'
    testSuite.run( testResults )
    print '#'*79
    if testResults.errors == [] and testResults.failures == []:
        print 'All unit-tests passed! Ready for build/copy!'
        import shutil
        sitePackagesDirs = []
        for path in sys.path:
            if path.split( os.sep )[-1].lower() == 'site-packages':
                sitePackagesDirs.append( path )
        for path in sitePackagesDirs:
            destinationFile = path + os.sep + __file__
            try:
                shutil.copyfile( './%s' % __file__, destinationFile )
                print 'Copied %s to %s' % ( __file__, path )
            except:
                print 'Could not copy file %s to location %s' % ( __file__, path )
    else:
        print 'Unit-tests failed (see items below)'
        print '#'*79
        if testResults.errors:
            print 'Errors'
            print '#' + '-'*77 + '#'
            for error in testResults.errors:
                print error[1]
                print '#' + '-'*77 + '#'
            print
        if testResults.failures:
            if testResults.errors:
                print '#'*79
            print 'Failures'
            print '#' + '-'*77 + '#'
            for failure in testResults.failures:
                print failure[1]
                print '#' + '-'*77 + '#'
            print
    print '#'*79

Commentary:
Line(s)
26, 32-37
I'm using the "public" members of the types module to generate a list of all of the available public types, except those that can be defined/re-defined by the developer (e.g., ClassType and TypeType, for classic- and new-style-classes, respectively, plus InstanceType and ObjectType so that we don't later try to use instances of either class-type as member-types). This code feels a bit strange to me, now that I look at it again, and though I know I had a reason for this approach, I cannot, for the life of me, remember what that reason was now. I'll have to ponder on it, but at the very least, it's functional.
52-138
The nominal abstract class that TypedDictionary and (eventually) the other typed containers are based on. It's primary purpose is to provide a consistent MemberTypes property for all of the derived container-classes it'll be a subclass of, but it also provides a mechanism (IsValidmemberType) to detect whether a given item is legitimately a member of the container.
Line(s)
63-66, 72-89, 95-98, 104
Property getter, setter, and deleter method definitions, and the formal property declaration for the MemberTypes property.
Note that _SetMemberTypes checks for basic types (lines 79-80), classic-class types (82-83), which aren't allowed at present because I haven't found a reliable way to detect whether their classes are subclasses of other (classic) classes, and new-style classes (85-86). Although I cannot imagine that there would be any other types that would come up (other than the instance types noted above), I've assured that the code will raise an error (88-89) if such a case occurs.
126-136
The mechanism mentioned for determining whether an item is a valid member of the container, by checking against it's defined member-types using Python's set data-types.
144-198
The actual TypedDictionary definition. There really isn't much to it, apart from the constructor and the overload of __setitem__:
For hopefully-obvious reasons, the constructor requires that an iterable argument of allowed member-types is supplied. It gets passed directly to the inherited _SetMemberTypes method we've already seen, which is not exposed publicly.
__setitem__ appeared to be the only method of the base dict class that needed to be overridden to type-ceck the value to be added before actually adding it. Rather than muck about with re-writing the base method, it's simply called after the valid-member-type check passes.
Since the whole point of the class is to raise a type error when an element is added that isn't of a valid type, that gets raised as well.
The unit-tests may look a bit convoluted (they felt that way to me when I was writing them, to be sure). Ultimately, all they are trying to do, though, is to assure that the member-type checks are tested with all of the logical permutations:
  • Simple value-types (singly or multiply);
  • New-style class-types (both directly or as subclasses of member-types); and
  • Classic-class types (which should, as noted above, all fail at this time).
Bearing that in mind, I think they're actually pretty straightforward...