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 ) )

No comments:

Post a Comment