So, now that the interface-definitions are out of the way, I'm going to tackle
the nominal abstract classes. There are only two, BaseDataConnector
that will provide structure for concrete data-connector classes like
MySQLConnector
(and others down the line, potentially), and
BaseDataObject
, which will provide CRUD
capabilities for objects whose state is going to be persisted in a back-end
data-store.
BaseDataConnector
provides standard connection parameters (host,
database, user and password) as properties for connector objects. It does
not define the connection property, or the mechanism used to connect
to the database, however, since those will vary from database-engine to
database-engine. A typical usage is expected to look something like the code
below, assuming that Connector
is a class derived from
BaseDataConnector
:
connection = Connector(
host='host',
database='database',
user='user',
password='password'
)
# Optionally, the "Connect" method can be called before executing a query:
# connection.Connect()
results = connection.Query( 'some SQL code' ).Results
This looks pretty simple, though there's a few things I still need to think
out (specifically, what I want the items in the results to look like). But even
at this point, there's a lot going on "under the hood," as it were, and some
possibilities that I'm contemplating for the future:
- The creation of the
Connector
handles all of the
connection-information for the back-end datasource - that much we pretty
much have to assume.
- Whether the code written against the Connector explicitly calls
Connect
or not, there has to be an active connection before
queries can be run, so the first call to a query's Results
property would perhaps have to tell the connector-object that it's ready
to query, and to create the connection.
- I have to admit that I like the idea of lazily-instantiating
queries, results, etc. - Provided that some care is taken to keep
things logical, I think that using a lazy instantiation would
potentially save some database-connection time.
- I also contemplated the idea of having one or more queues of
queries associated with the connector-object: One for queries to
be executed at object destruction, and one to allow queries to be
generated and stored before they were actually needed, which
would allow the first "real" need for a query's results to run
all of the pending queries in one connection-batch,
maybe. I'm not sure how much (if any) difference this would
actually make, and it's potentially more complicated than I want
to deal with right this minute, though, so I'm filing that away
for future reference/use.
- The
results
returned could/should be allowed to support
multiple assignments (e.g., results1, results2 = connection.Query(
'some SQL code' ).Results
) as well as single results. I'm not
quite sure how I'm going to handle that just yet...
At the nominal-abstract-class level, there's not a lot that can realistically
be implemented - mostly standard properties and the like - so the code is pretty
short:
BaseDataConnector (Nominal abstract class):
class BaseDataConnector( IsDataConnector, IsConfigurable, object ):
"""Provides baseline functionality, interface requirements and type-identity for objects that can represent a connection to a back-end database."""
##################################
# Class Attributes #
##################################
##################################
# Class Property-Getter Methods #
##################################
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _GetDatabase( self ):
"""Gets the name of the database that the connection will be made to."""
return self._database
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _GetHost( self ):
"""Gets the name or IP-address of the host that the database resides upon."""
return self._host
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _GetPassword( self ):
"""Gets the password used to connect to the database."""
return self._password
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _GetUser( self ):
"""Gets the user-name used to connect to the database."""
return self._user
##################################
# Class Property-Setter Methods #
##################################
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
@DocumentArgument( 'argument', 'value', None, '(String, required) The name of the database that the connection will be made to.' )
def _SetDatabase( self, value ):
"""Sets the name of the database that the connection will be made to."""
if type( value ) != types.StringType:
raise TypeError( '%s.Database expects a single-line, non-empty string value that contains to tabs.' % ( self.__class__.__name__ ) )
if value == '' or '\n' in value or '\r' in value or '\t' in value:
raise ValueError( '%s.Database expects a single-line, non-empty string value that contains to tabs.' % ( self.__class__.__name__ ) )
self._database = value
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
@DocumentArgument( 'argument', 'value', None, '(String, required) The name of the Host that the connection will be made to.' )
def _SetHost( self, value ):
"""Sets the name of the Host that the connection will be made to."""
if type( value ) != types.StringType:
raise TypeError( '%s.Host expects a single-line, non-empty string value that contains to tabs.' % ( self.__class__.__name__ ) )
if value == '' or '\n' in value or '\r' in value or '\t' in value:
raise ValueError( '%s.Host expects a single-line, non-empty string value that contains to tabs.' % ( self.__class__.__name__ ) )
self._host = value
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
@DocumentArgument( 'argument', 'value', None, '(String, required) The password that the connection will be made with.' )
def _SetPassword( self, value ):
"""Sets the password that the connection will be made with."""
if type( value ) != types.StringType:
raise TypeError( '%s.Password expects a single-line, non-empty string value that contains to tabs.' % ( self.__class__.__name__ ) )
if value == '' or '\n' in value or '\r' in value or '\t' in value:
raise ValueError( '%s.Password expects a single-line, non-empty string value that contains to tabs.' % ( self.__class__.__name__ ) )
self._password = value
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
@DocumentArgument( 'argument', 'value', None, '(String, required) The name of the User that the connection will be made with.' )
def _SetUser( self, value ):
"""Sets the name of the User that the connection will be made with."""
if type( value ) != types.StringType:
raise TypeError( '%s.User expects a single-line, non-empty string value that contains to tabs.' % ( self.__class__.__name__ ) )
if value == '' or '\n' in value or '\r' in value or '\t' in value:
raise ValueError( '%s.User expects a single-line, non-empty string value that contains to tabs.' % ( self.__class__.__name__ ) )
self._user = value
##################################
# Class Property-Deleter Methods #
##################################
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _DelDatabase( self ):
"""Deletes the name of the database that the connection will be made to."""
self._database = None
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _DelHost( self ):
"""Deletes the name or IP-address of the host that the database resides upon."""
self._host = None
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _DelPassword( self ):
"""Deletes the password used to connect to the database."""
self._password = None
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _DelUser( self ):
"""Deletes the user-name used to connect to the database."""
self._user = None
##################################
# Class Properties #
##################################
Database = property( _GetDatabase, _SetDatabase, _DelDatabase, 'Gets, sets or deletes the name of the database that the connection will be made to.' )
Host = property( _GetHost, _SetHost, _DelHost, 'Gets, sets or deletes the name or IP-address of the host that the database resides upon.' )
Password = property( _GetPassword, _SetPassword, _DelPassword, 'Gets, sets or deletes the password used to connect to the database.' )
User = property( _GetUser, _SetUser, _DelUser, 'Gets, sets or deletes the user-name used to connect to the database.' )
##################################
# Object Constructor #
##################################
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
@DocumentArgument( 'keyword', 'parameters', 'host', 'The name or IP-address of the host that the database resides upon.' )
@DocumentArgument( 'keyword', 'parameters', 'database', 'The name of the database that the connection will be made to.' )
@DocumentArgument( 'keyword', 'parameters', 'user', 'The user-name used to connect to the database.' )
@DocumentArgument( 'keyword', 'parameters', 'password', 'The password used to connect to the database.' )
def __init__( self, **parameters ):
"""Object constructor."""
# Nominally abstract: Don't allow instantiation of the class
if self.__class__ == BaseDataConnector:
raise NotImplementedError( 'BaseDataConnector is (nominally) an abstract class, and is not intended to be instantiated.' )
self._DelDatabase()
if parameters.has_key( 'database' ):
self._SetDatabase( parameters[ 'database' ] )
self._DelHost()
if parameters.has_key( 'host' ):
self._SetHost( parameters[ 'host' ] )
self._DelPassword()
if parameters.has_key( 'password' ):
self._SetPassword( parameters[ 'password' ] )
self._DelUser()
if parameters.has_key( 'user' ):
self._SetUser( parameters[ 'user' ] )
##################################
# Object Destructor #
##################################
##################################
# Class Methods #
##################################
##################################
# IsConfigurable Methods #
##################################
@ToDo( 'Figure out how to approach unit-testing of this method.' )
@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', 'host', 'The name or IP-address of the host that the database resides upon.' )
@DocumentConfiguration( 'Section Name', 'database', 'The name of the database that the connection will be made to.' )
@DocumentConfiguration( 'Section Name', 'user', 'The user-name used to connect to the database.' )
@DocumentConfiguration( 'Section Name', 'password', 'The password used to connect to the database.' )
def Configure( self, configProvider, configSection ):
"""Configures the object using configuration data from the specified section of the specified provider."""
parameters = configProvider[ configSection ]
if parameters.has_key( 'database' ):
self._SetDatabase( parameters[ 'database' ] )
if parameters.has_key( 'host' ):
self._SetHost( parameters[ 'host' ] )
if parameters.has_key( 'password' ):
self._SetPassword( parameters[ 'password' ] )
if parameters.has_key( 'user' ):
self._SetUser( parameters[ 'user' ] )
##################################
# IsDataConnector Methods #
##################################
# Leaving Connect method abstracted from IsDataConnector
# Leaving Query method abstracted from IsDataConnector
__all__ += [ 'BaseDataConnector' ]
- Line(s)
- 12-30
- Typical (by now) property-getter methods.
- 36-74
- Typical (by now) property-setter methods.
- 80-98
- I'm generating explicit Deleter methods as well, since I want the properties
to be "deletable," but without allowing actual deletion - instead
I want them to be set to
None
. Partly this is so that I've
got an easy way to set them to their default values during construction
or configuration, but mostly it's because I anticipate that in actual
implementations I'll need to check whether they're set or not, and I'd
rather that they always have at least a None
value,
rather than raising AttributeError
s if they get accidentally
deleted.
- 104-107
- The "real" property declarations.
- 113-134
- The object constructor. Note that we're using a keyword-list style argument,
documenting the recognized names, and using the class'
_Set...
methods to actually set the values. We could use the properties
that those methods map to, but by going directly to the methods themselves,
if the "settability" of the property is changed, that won't introduce
errors.
- 148-166
- Object configuration. To keep things simple, it uses the same names as
the object constructor.
- I'm not sure how I want to approach the unit-testing of this method just
yet (as noted in the ToDo on 148). I'll have to give that some thought,
and I'll likely generate a "clean-up" post once everything else is done
that will address that and any other items that seem relevant, but for
now, I'm going to leave this untested.
Unit-tests
class BaseDataConnectorDerived( BaseDataConnector ):
def __init__( self, **parameters ):
BaseDataConnector.__init__( self, **parameters )
class testBaseDataConnector( unittest.TestCase ):
"""Unit-tests the BaseDataConnector class."""
def setUp( self ):
pass
def tearDown( self ):
pass
def testDerived( self ):
"""Testing abstract nature of the BaseDataConnector class."""
try:
testObject = BaseDataConnector()
self.fail( 'BaseDataConnector is nominally abstract, and should not be instantiable.' )
except NotImplementedError:
pass
except Exception, error:
self.fail( 'BaseDataConnector is nominally abstract, and should raise NotImplementedError when instantiated, but %s was raised instead\n %s' % ( error.__class__.__name__, error ) )
testObject = BaseDataConnectorDerived()
self.assertTrue( isinstance( testObject, BaseDataConnector ) , 'BaseDataConnector-derived objects should be instances of BaseDataConnector' )
def testPropertyCountAndTests( self ):
"""Testing the properties of the BaseDataConnector class."""
items = getMemberNames( BaseDataConnector )[0]
actual = len( items )
expected = 5
self.assertEquals( expected, actual, 'BaseDataConnector 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 BaseDataConnector class."""
items = getMemberNames( BaseDataConnector )[1]
actual = len( items )
expected = 3
self.assertEquals( expected, actual, 'BaseDataConnector 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 BaseDataConnector abstract class."""
self.assertEquals( BaseDataConnector.Connection, IsDataConnector.Connection, 'BaseDataConnector implements IsdataConnector, but should not define the Connection property.' )
def testDatabase( self ):
"""Unit-tests the Database property of the BaseDataConnector abstract class."""
goodValues = [ 'databasename', 'database_name', 'a' ]
for testValue in goodValues:
testObject = BaseDataConnectorDerived()
testObject.Database = testValue
self.assertEquals( testObject.Database, testValue, 'The database property, if set, should get the set value.' )
testObject = BaseDataConnectorDerived( database=testValue )
self.assertEquals( testObject.Database, testValue, 'The database property, if set, should get the set value.' )
badTypes = [ None, True, 0, [] ]
for testValue in badTypes:
testObject = BaseDataConnectorDerived()
try:
testObject.Database = testValue
self.fail( 'Setting Database to %s should raise a TypeError.' )
except TypeError:
pass
except Exception, error:
self.fail( 'Setting Database to %s should raise a TypeError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) )
try:
testObject = BaseDataConnectorDerived( database=testValue)
self.fail( 'Creating a BaseDataConnector with a database value of %s should raise a TypeError.' )
except TypeError:
pass
except Exception, error:
self.fail( 'Creating a BaseDataConnector with a database value of %s should raise a TypeError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) )
badValues = [ '', 'bad\nvalue', 'bad\tvalue', 'bad\rvalue' ]
for testValue in badValues:
testObject = BaseDataConnectorDerived()
try:
testObject.Database = testValue
self.fail( 'Setting Database to %s should raise a ValueError.' )
except ValueError:
pass
except Exception, error:
self.fail( 'Setting Database to %s should raise a ValueError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) )
try:
testObject = BaseDataConnectorDerived( database=testValue)
self.fail( 'Creating a BaseDataConnector with a database value of %s should raise a ValueError.' )
except ValueError:
pass
except Exception, error:
self.fail( 'Creating a BaseDataConnector with a database value of %s should raise a ValueError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) )
def testHost( self ):
"""Unit-tests the Host property of the BaseDataConnector abstract class."""
goodValues = [ 'Hostname', 'Host_name', 'a' ]
for testValue in goodValues:
testObject = BaseDataConnectorDerived()
testObject.Host = testValue
self.assertEquals( testObject.Host, testValue, 'The Host property, if set, should get the set value.' )
testObject = BaseDataConnectorDerived( host=testValue )
self.assertEquals( testObject.Host, testValue, 'The Host property, if set, should get the set value.' )
badTypes = [ None, True, 0, [] ]
for testValue in badTypes:
testObject = BaseDataConnectorDerived()
try:
testObject.Host = testValue
self.fail( 'Setting Host to %s should raise a TypeError.' )
except TypeError:
pass
except Exception, error:
self.fail( 'Setting Host to %s should raise a TypeError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) )
try:
testObject = BaseDataConnectorDerived( host=testValue)
self.fail( 'Creating a BaseDataConnector with a Host value of %s should raise a TypeError.' )
except TypeError:
pass
except Exception, error:
self.fail( 'Creating a BaseDataConnector with a Host value of %s should raise a TypeError, but %s was raised instead:\n %s' % ( testValue, error.__class__.__name__, error ) )
badValues = [ '', 'bad\nvalue', 'bad\tvalue', 'bad\rvalue' ]
for testValue in badValues:
testObject = BaseDataConnectorDerived()
try:
testObject.Host = testValue
self.fail( 'Setting Host to %s should raise a ValueError.' % ( testValue ) )
except ValueError:
pass
except Exception, error:
self.fail( 'Setting Host to %s should raise a ValueError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) )
try:
testObject = BaseDataConnectorDerived( host=testValue)
self.fail( 'Creating a BaseDataConnector with a Host value of %s should raise a ValueError.' )
except ValueError:
pass
except Exception, error:
self.fail( 'Creating a BaseDataConnector with a Host value of %s should raise a ValueError, but %s was raised instead:\n %s' % ( testValue, error.__class__.__name__, error ) )
def testPassword( self ):
"""Unit-tests the Password property of the BaseDataConnector abstract class."""
goodValues = [ 'password', 'pass_word', 'a' ]
for testValue in goodValues:
testObject = BaseDataConnectorDerived()
testObject.Password = testValue
self.assertEquals( testObject.Password, testValue, 'The Password property, if set, should get the set value.' )
testObject = BaseDataConnectorDerived( password=testValue )
self.assertEquals( testObject.Password, testValue, 'The Password property, if set, should get the set value.' )
badTypes = [ None, True, 0, [] ]
for testValue in badTypes:
testObject = BaseDataConnectorDerived()
try:
testObject.Password = testValue
self.fail( 'Setting Password to %s should raise a TypeError.' )
except TypeError:
pass
except Exception, error:
self.fail( 'Setting Password to %s should raise a TypeError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) )
try:
testObject = BaseDataConnectorDerived( password=testValue )
self.fail( 'Creating a BaseDataConnector with a password value of %s should raise a TypeError.' % ( testValue ) )
except TypeError:
pass
except Exception, error:
self.fail( 'Creating a BaseDataConnector with a password value of %s should raise a TypeError, but %s was raised instead:\n %s' % ( testValue, error.__class__.__name__, error ) )
badValues = [ '', 'bad\nvalue', 'bad\tvalue', 'bad\rvalue' ]
for testValue in badValues:
testObject = BaseDataConnectorDerived()
try:
testObject.Password = testValue
self.fail( 'Setting Password to %s should raise a ValueError.' % ( testValue ) )
except ValueError:
pass
except Exception, error:
self.fail( 'Setting Password to %s should raise a ValueError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) )
try:
testObject = BaseDataConnectorDerived( password=testValue)
self.fail( 'Creating a BaseDataConnector with a password value of %s should raise a ValueError.' )
except ValueError:
pass
except Exception, error:
self.fail( 'Creating a BaseDataConnector with a password value of %s should raise a ValueError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) )
def testUser( self ):
"""Unit-tests the User property of the BaseDataConnector abstract class."""
goodValues = [ 'User', 'user_name', 'a' ]
for testValue in goodValues:
testObject = BaseDataConnectorDerived()
testObject.User = testValue
self.assertEquals( testObject.User, testValue, 'The User property, if set, should get the set value.' )
testObject = BaseDataConnectorDerived( user=testValue )
self.assertEquals( testObject.User, testValue, 'The User property, if set, should get the set value.' )
badTypes = [ None, True, 0, [] ]
for testValue in badTypes:
testObject = BaseDataConnectorDerived()
try:
testObject.User = testValue
self.fail( 'Setting User to %s should raise a TypeError.' )
except TypeError:
pass
except Exception, error:
self.fail( 'Setting User to %s should raise a TypeError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) )
try:
testObject = BaseDataConnectorDerived( user=testValue )
self.fail( 'Creating a BaseDataConnector with a User value of %s should raise a TypeError.' % ( testValue ) )
except TypeError:
pass
except Exception, error:
self.fail( 'Creating a BaseDataConnector with a User value of %s should raise a TypeError, but %s was raised instead:\n %s' % ( testValue, error.__class__.__name__, error ) )
badValues = [ '', 'bad\nvalue', 'bad\tvalue', 'bad\rvalue' ]
for testValue in badValues:
testObject = BaseDataConnectorDerived()
try:
testObject.User = testValue
self.fail( 'Setting User to %s should raise a ValueError.' % ( testValue ) )
except ValueError:
pass
except Exception, error:
self.fail( 'Setting User to %s should raise a ValueError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) )
try:
testObject = BaseDataConnectorDerived( user=testValue)
self.fail( 'Creating a BaseDataConnector with a User value of %s should raise a ValueError.' )
except ValueError:
pass
except Exception, error:
self.fail( 'Creating a BaseDataConnector with a User value of %s should raise a ValueError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) )
# Unit-test methods
def testConfigure( self ):
"""Unit-tests the Configure method of the BaseDataConnector abstract class."""
# Not sure how I want to test this yet. See ToDo on Configure.
pass
def testConnect( self ):
"""Unit-tests the Connect method of the BaseDataConnector abstract class."""
self.assertEquals( BaseDataConnector.Connect, IsDataConnector.Connect, 'BaseDataConnector should not define the Connect method.' )
def testQuery( self ):
"""Unit-tests the Query method of the BaseDataConnector abstract class."""
self.assertEquals( BaseDataConnector.Query, IsDataConnector.Query, 'BaseDataConnector should not define the Query method.' )
testSuite.addTests( unittest.TestLoader().loadTestsFromTestCase( testBaseDataConnector ) )
With the exception of the method-tests, this should be pretty straightforward by now - it's my typical structure...
The method-tests, though, may deserve some explanation. I noted above that I
wasn't sure how I wanted to test the Configure
method (as much as
anything else because it, ideally, should be tested against real configuration-files,
and I'm not sure I want to generate the sheer number of files that would be needed).
For now, I'm going to leave that method untested, though I'll have to get
back to that fairly soon to maintain my code-coverage goals for code tied to my
posts.
The tests for Connect
and Query
are pretty simple,
but what they do may not be readily apparent. Each of these is really
just making sure that there is no local method (e.g., that BaseDataConnector
doesn't define them, but inherits the nominally-abstract stubs from
IsDataConnector
).
The next nominally abstract class to tackle is BaseDataObject
.
The intention behind BaseDataObject
is to provide common CRUD
functionality for derived objects, as noted previously. In order to try to keep
the actual read/write/update/delete process as simple as possible, creation of
and update to a state-data record is going to be wrapped in a Save
method that pays attention to various flag-value properties:
- IsDeleted
- Indicates whether an object's state-data record is pseudo-deleted.
It's not uncommon, at least in my experience, to save even "deleted"
records in a system for any of several reasons, practical and legal
alike. This flag exists in order to assure that data-objects can be
saved while being easily flagged as deleted.
- IsDirty
- Indicates whether an object's state-data record needs to be
updated in the back-end data-store. This means that objects
deriving from
BaseDataObject
will have to make sure that
when a persistent state value changes, the entire object is flagged
as "dirty".
- IsNew
- Indicates whether an object's state-data record needs to be
created in the back-end data-store. When a record is created,
the unique idenitifier of the record will need to be returned in some
fashion, and stored in the object's state if at all possible.
These properties have implications on the design of database-tables that store
object state-data: They should ideally have a unique identifier (which is generally
desirable anyway), and a flag-field that indicates whether the state-record is
"deleted" (or is active, if you prefer that approach). Other common fields (creation
date, last modified date) could be added easily enough to BaseDataObject
,
but they feel less... globally used, I guess, to me.
The IsDirty
and IsNew
flags raise some questions as
well. Ideally, I think, no given object should ever be considered both New and Dirty, since
one implies that the object-record hasn't been created, while the other implies
that it has, and on top of that, that it's been modified since it was
created. An object that is created in an application and that will be saved to a
database should, therefore, be considered "new" until it has been saved (at which
point both flags should be False). Changes to that object's state before it's
saved should also make the object "dirty," logically, but we don't
want both a creation and an update to happen on the same object when it's
being saved. I believe that all of this should be handled at the Save
-method
level, with logic that would look something like this:
- When an object is created, it is new and not dirty; Saving it should
create a state-record.
- When that object is modified, it is both new and dirty; Saving it
should create a state-record.
- When that object is pseudo-deleted, it is both new and deleted;
Saving it should create a state-record, and that record should indicate
that it is "deleted".
- When an object is retrieved from the database, it is not new, nor is
it dirty; Saving it should do nothing, if only to avoid unneeded
database connections/interaction.
- When a retrieved object is modified, it is dirty but still not new;
Saving it should cause an update.
- When a retrieved object is pseudo-deleted, it is both dirty and deleted;
Saving it should cause an update.
- Any of these changes should be accounted for when the object is destroyed,
in order to assure that state-data is correctly persisted.
BaseDataObject (Nominal abstract class):
class BaseDataObject( object ):
"""Provides baseline functionality, interface requirements and type-identity for objects whose state can be persisted in a back-end database."""
##################################
# Class Attributes #
##################################
##################################
# Class Property-Getter Methods #
##################################
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _GetDataSource( self ):
"""Gets the data-source (an IsDataConnector instance) that the data-object will use to perform it's queries against."""
return self._dataSource
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _GetId( self ):
"""Gets the unique ID of the object's state-data-record in the back-end database."""
return self._id
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _GetIsDeleted( self ):
"""Gets the flag indicating whether the object's state-data record is "deleted"."""
return self._isDeleted
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _GetIsDirty( self ):
"""Gets the flag indicating whether the object's state-data record is "Dirty"."""
return self._isDirty
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _GetIsNew( self ):
"""Gets the flag indicating whether the object's state-data record is "New"."""
return self._isNew
##################################
# Class Property-Setter Methods #
##################################
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
@DocumentArgument( 'argument', 'value', None, 'The data-source (an IsDataConnector instance) that the data-object will use to perform it\'s queries against.' )
def _SetDataSource( self, value ):
"""Sets the data-source (an IsDataConnector instance) that the data-object will use to perform it's queries against."""
if self._dataSource:
raise NotImplementedError( '%s.DataSource cannot be changed after it has been set.' % ( self.__class__.__name__ ) )
if not isinstance( value, IsDataConnector ):
raise TypeError( '%s.DataSource expects an instance of IsDataConnector.' % ( self.__class__.__name__ ) )
self._dataSource = value
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
@DocumentArgument( 'argument', 'value', None, 'The unique ID of the object\'s state-data-record in the back-end database.' )
def _SetId( self, value ):
"""Sets the unique ID of the object's state-data-record in the back-end database."""
self._id = value
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
@DocumentArgument( 'argument', 'value', None, 'The flag indicating whether the object\'s state-data record is "deleted".' )
def _SetIsDeleted( self, value ):
"""Sets the flag indicating whether the object's state-data record is "deleted"."""
if value != True and value != False and value != 1 and value != 0:
raise TypeError( '%s.IsDeleted expects a True, False, 1 or 0 value.' % ( self.__class__.__name__ ) )
self._isDeleted = value
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
@DocumentArgument( 'argument', 'value', None, 'The flag indicating whether the object\'s state-data record is "dirty" (in need of an update).' )
def _SetIsDirty( self, value ):
"""Sets the flag indicating whether the object's state-data record is "dirty" (in need of an update)."""
if value != True and value != False and value != 1 and value != 0:
raise TypeError( '%s.IsDirty expects a True, False, 1 or 0 value.' % ( self.__class__.__name__ ) )
self._isDirty = value
if value:
self._isNew = False
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
@DocumentArgument( 'argument', 'value', None, 'The flag indicating whether the object\'s state-data record is "New".' )
def _SetIsNew( self, value ):
"""Sets the flag indicating whether the object's state-data record is "New"."""
if value != True and value != False and value != 1 and value != 0:
raise TypeError( '%s.IsNew expects a True, False, 1 or 0 value.' % ( self.__class__.__name__ ) )
self._isNew = value
##################################
# Class Property-Deleter Methods #
##################################
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _DelDataSource( self ):
"""Deletes the data-source that the data-object will use to perform it's queries against."""
self._dataSource = None
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _DelId( self ):
"""Deletes the data-source that the data-object will use to perform it's queries against."""
self._id = None
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _DelIsDeleted( self ):
"""Deletes the flag indicating whether the object's state-data record is "Deleted"."""
self._isDeleted = None
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _DelIsDirty( self ):
"""Deletes the flag indicating whether the object's state-data record is "dirty" (in need of an update)."""
self._isDirty = None
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _DelIsNew( self ):
"""Deletes the flag indicating whether the object's state-data record is "New" (in need of creation)."""
self._isNew = None
##################################
# Class Properties #
##################################
DataSource = property( _GetDataSource, _SetDataSource, None, 'Gets or sets the data-source (an IsDataConnector instance) that the data-object will use to perform it\'s queries against.' )
Id = property( _GetId, None, None, 'Gets the unique identifier of the object\'s state-data record.')
IsDeleted = property( _GetIsDeleted, _SetIsDeleted, None, 'Gets or sets the flag indicating that the object\'s state-data record is pseudo-deleted.' )
IsDirty = property( _GetIsDirty, _SetIsDirty, None, 'Gets or sets the flag indicating that the object\'s state-data record is "dirty," and in need of an update.' )
IsNew = property( _GetIsNew, None, None, 'Gets or sets the flag indicating that the object\'s state-data record is "New," and needs to be created.' )
##################################
# Object Constructor #
##################################
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def __init__( self ):
"""Object constructor."""
# Set new and dirty flags before checking for instantiability, in order to
# avoid raising errors at destruction.
self._isDirty = None
self._isNew = None
# Nominally abstract: Don't allow instantiation of the class
if self.__class__ == BaseDataObject:
raise NotImplementedError( 'BaseDataObject is (nominally) an abstract class, and is not intended to be instantiated.' )
self._DelDataSource()
self._DelId()
self.IsDeleted = False
self.IsDirty = False
self._isNew = True
##################################
# Object Destructor #
##################################
def __del__( self ):
"""Object destructor.
Saves the object's state-data before destruction."""
self.Save()
##################################
# Class Methods #
##################################
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _Create( self ):
"""(Nominal abstract method) Creates a record in the back-end database for the object's state-data, updating the object's Id property with the identity of the created record."""
raise NotImplementedError( '%s._Create has not been implemented as required by BaseDataObject.' % ( self.__class__.__name__ ) )
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def Delete( self ):
"""(Nominal abstract method) Performs a physical record-deletion of a specific object's state-data record from the back-end database."""
raise NotImplementedError( '%s.Delete has not been implemented as required by BaseDataObject.' % ( self.__class__.__name__ ) )
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
@DocumentArgument( 'argument', 'id', None, 'The unique identifier for the state-data record of the object to be retrieved from the database.' )
def Read( self, id ):
"""(Nominal abstract method) Reads a specific object's state-data record from the back-end database, and returns an object-instance with the retrieved state-data."""
raise NotImplementedError( '%s.Read has not been implemented as required by BaseDataObject.' % ( self.__class__.__name__ ) )
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def Save( self ):
"""Creates or updates the object's state-data record based on whether the object's IsNew or IsDirty flag is set."""
if self._isNew:
self._Create()
elif self._isDirty:
self._Update()
self.IsDirty = False
self._isNew = False
@DocumentArgument( 'argument', 'self', None, SelfDocumentationString )
def _Update( self ):
"""(Nominal abstract method) Updates the object's state-data record in the back-end database."""
raise NotImplementedError( '%s._Update has not been implemented as required by BaseDataObject.' % ( self.__class__.__name__ ) )
__all__ += [ 'BaseDataObject' ]
Commentary
- Line(s)
- 13-35
- Typical property-getter structures.
- 42-49
- Standard property-setter structure.
- 51-55
- The property-setter for the
Id
property is slightly different
than the typical pattern I've shown in previous code, in that it just sets
the value, performing no type- or value-checking. Id
could,
arguably, be left abstract at the BaseDataObject
level, but
there doesn't seem to me to be any significant advantage to doing so - I'd
rather have a single point of definition, and (if necessary) override the
setter method in derived classes to provide type- and/or value-checking
when or if necessary. I have a lingering suspicion that I'll regret that
design decision somewhere along the line, but for the time being, I'll
hold to it, and seee what happens.
- 58-63, 66-72, 75-80
- Since the properties behind these setters are boolean values, there's no
real need to do a type-check them - a simple check of all of the allowed
values should suffice.
- 71-73
- Since setting the object's state to "dirty" should reset it's
"new" status, we're doing that here, but only if the
"dirty" status is
True
(or equivalent).
- 88-110
- Typical property-deleter method-structure.
- 116-120
- Typical property definitions.
- 146-150
- By default, any object derived from
BaseDataObject
should
save it's state before it is destroyed. Since the _Create
and _Update
methods are still abstract (and thus need to be
defined by the derived classes), but still exist at the
BaseDataObject
level, the destructor can take advantage of
the methods' existence, define the Save
method, and call
it in the object destructor.
- 157-159, 162-164, 168-170, 183-185
- The nominally-abstract methods for state-data-record creation, deletion,
read/retrieval and update, respectively. These must be abstract at this
level, because there is no way to anticipate exactly how they
will work, but we know that they need to exist.
- 173-180
- The concrete method that creates or updates a state-data record for the
derived object, based on the object's "dirty" or "new" state.
For the most part, the unit-tests are pretty much what you'd expect, particularly
if you've been following this blog for any length of time:
class BaseDataObjectDerived( BaseDataObject ):
def __init__( self ):
BaseDataObject.__init__( self )
def __del__( self ):
pass
class BaseDataObjectImplemented( BaseDataObject ):
def __init__( self ):
BaseDataObject.__init__( self )
def __del__( self ):
BaseDataObject.__del__( self )
def _Create( self ):
fp = open( '/tmp/create', 'w' )
fp.write( '_Create' )
fp.close()
def _Update( self ):
fp = open( '/tmp/update', 'w' )
fp.write( '_Update' )
fp.close()
class testBaseDataObject( unittest.TestCase ):
"""Unit-tests the BaseDataObject class."""
def setUp( self ):
pass
def tearDown( self ):
try:
os.unlink( '/tmp/create' )
except:
pass
try:
os.unlink( '/tmp/update' )
except:
pass
def testDerived( self ):
"""Testing abstract nature of the BaseDataObject class."""
try:
testObject = BaseDataObject()
self.fail( 'BaseDataObject is nominally an abstract class, and should not be instantiable.' )
except NotImplementedError:
pass
except Exception, error:
self.fail( 'Attempting to instantiate a BaseDataObject should raise NotImplementedError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) )
testObject = BaseDataObjectDerived()
self.assertTrue( isinstance( testObject, BaseDataObject ), 'Objects derived from BaseDataObject should be instances of BaseDataObject.' )
def testDestruction( self ):
"""Testing destruction of the BaseDataObject class."""
testObject = BaseDataObjectImplemented()
del testObject
try:
fp = open( '/tmp/create', 'r' )
fp.close()
except IOError:
self.fail( 'Destruction of a new object should result in it\'s _Create method being called.' )
except Exception, error:
self.fail( 'Destruction of a new object should result in it\'s _Create method being called, but %s was raised"\n %s' % ( error.__class__.__name__, error ) )
testObject = BaseDataObjectImplemented()
testObject.IsDirty = True
del testObject
try:
fp = open( '/tmp/update', 'r' )
fp.close()
except IOError:
self.fail( 'Destruction of a modified object should result in it\'s _Update method being called.' )
except Exception, error:
self.fail( 'Destruction of a modified object should result in it\'s _Update method being called, but %s was raised"\n %s' % ( error.__class__.__name__, error ) )
def testPropertyCountAndTests( self ):
"""Testing the properties of the BaseDataObject class."""
items = getMemberNames( BaseDataObject )[0]
actual = len( items )
expected = 5
self.assertEquals( expected, actual, 'BaseDataObject 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 BaseDataObject class."""
items = getMemberNames( BaseDataObject )[1]
actual = len( items )
expected = 3
self.assertEquals( expected, actual, 'BaseDataObject 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 ):
"""Unit-tests the DataSource property of the BaseDataObject nominal abstract class."""
testValue = IsDataConnectorDerived()
testObject = BaseDataObjectDerived()
testObject.DataSource = testValue
self.assertEquals( testObject.DataSource, testValue, 'Setting the DataSource should return the set value on a get.' )
badValues = [ None, True, 1, 'ook' ]
for testValue in badValues:
try:
testObject = BaseDataObjectDerived()
testObject.DataSource = testValue
self.fail( 'Setting DataSource to a value of %s should raise a TypeError' % ( testValue ) )
except TypeError:
pass
except Exception, error:
self.fail( 'Setting DataSource to a value of %s should raise a TypeError, but %s was raised instead:\n %s' % ( testValue, error.__class__.__name__, error ) )
testObject = BaseDataObjectDerived()
testValue = IsDataConnectorDerived()
testValue2 = IsDataConnectorDerived()
testObject.DataSource = testValue
try:
testObject.DataSource = testValue2
self.fail( 'Changing an established DataSource of a BaseDataObject should not be allowed' )
except NotImplementedError:
pass
except Exception, error:
self.fail( 'Changing an established DataSource of a BaseDataObject should raise NotImplementedError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) )
def testId( self ):
"""Unit-tests the Id property of the BaseDataObject nominal abstract class."""
# There really isn't any useful test that can be done here without
# mandating an ID type or structure, so this test really doesn't do
# anything except get counted as a testable property.
pass
def testIsDeleted( self ):
"""Unit-tests the IsDeleted property of the BaseDataObject nominal abstract class."""
testObject = BaseDataObjectDerived()
goodValues = [ True, False, 1, 0 ]
for testValue in goodValues:
testObject.IsDeleted = testValue
self.assertEquals( testObject.IsDeleted, testValue, 'Setting the IsDeleted value should returnt he same value when gotten.' )
badValues = [ None, 2, 'ook', -1, object() ]
for testValue in badValues:
try:
testObject.IsDeleted = testValue
self.fail( 'Setting IsDeleted to a value of %s should raise TypeError' % ( testValue ) )
except TypeError:
pass
except Exception, error:
self.fail( 'Setting IsDeleted to a value of %s should raise TypeError, but %s was raised instead:\n %s' % ( testValue, error.__class__.__name__, error ) )
def testIsDirty( self ):
"""Unit-tests the IsDirty property of the BaseDataObject nominal abstract class."""
testObject = BaseDataObjectDerived()
goodValues = [ True, False, 1, 0 ]
for testValue in goodValues:
testObject.IsDirty = testValue
self.assertEquals( testObject.IsDirty, testValue, 'Setting the IsDirty value should returnt he same value when gotten.' )
badValues = [ None, 2, 'ook', -1, object() ]
for testValue in badValues:
try:
testObject.IsDirty = testValue
self.fail( 'Setting IsDirty to a value of %s should raise TypeError' % ( testValue ) )
except TypeError:
pass
except Exception, error:
self.fail( 'Setting IsDirty to a value of %s should raise TypeError, but %s was raised instead:\n %s' % ( testValue, error.__class__.__name__, error ) )
def testIsNew( self ):
"""Unit-tests the IsNew property of the BaseDataObject nominal abstract class."""
testObject = BaseDataObjectDerived()
self.assertEquals( testObject.IsNew, True )
# self.assertEquals( testObject.IsNew, True, 'A default BaseDataObject-derived object should start with IsNew of True' )
# Test methods
def testDelete( self ):
"""Unit-tests the Delete method of the BaseDataObject nominal abstract class."""
testObject = BaseDataObjectDerived()
try:
testObject.Delete()
self.fail( 'BaseDataObject should not implement Delete.' )
except NotImplementedError:
pass
except Exception, error:
self.fail( 'BaseDataObject.Delete should raise NotImplementedError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) )
def testRead( self ):
"""Unit-tests the Read method of the BaseDataObject nominal abstract class."""
testObject = BaseDataObjectDerived()
try:
testObject.Read( None )
self.fail( 'BaseDataObject should not implement Read.' )
except NotImplementedError:
pass
except Exception, error:
self.fail( 'BaseDataObject.Read should raise NotImplementedError, but %s was raised instead:\n %s' % ( error.__class__.__name__, error ) )
def testSave( self ):
"""Unit-tests the Save method of the BaseDataObject nominal abstract class."""
testObject = BaseDataObjectImplemented()
testObject.Save()
try:
fp = open( '/tmp/create', 'r' )
fp.close()
except IOError:
self.fail( 'Save called on a new object should result in it\'s _Create method being called.' )
except Exception, error:
self.fail( 'Save called on a new object should result in it\'s _Create method being called, but %s was raised:\n %s.' % ( error.__class__.__name__, error ) )
testObject.IsDirty = True
testObject.Save()
try:
fp = open( '/tmp/create', 'r' )
fp.close()
except IOError:
self.fail( 'Save called on an updated object should result in it\'s _Create method being called.' )
except Exception, error:
self.fail( 'Save called on an updated object should result in it\'s _Create method being called, but %s was raised:\n %s.' % ( error.__class__.__name__, error ) )
testSuite.addTests( unittest.TestLoader().loadTestsFromTestCase( testBaseDataObject ) )
- Line(s)
- 1-5, 7-19
- Since there's a need to test both the default, unimplemented methods as
well as whether those methods get called as expected, there are actually
two derived test-classes that are used. The first,
BaseDataObjectDerived
is used to make sure that any unimplemented
abstract methods raise the exceptions that they should (a key part of
making sure that the nominally-abstract class will respond with expected
abstract-like behavior). The second, BaseDataObjectImplemented
,
provides implementations for _Create
and _Update
that write to files that the test-methods can access - these are a fair,
but low-weight representation of the basic kind of data-persistence
process that would be implemented (storing data to some external source),
though in this case, it's file-based instead of being tied to a database
(which we'd want to abstract away anyway).
- 27-35
- Since some of the test-methods are writing files, and a failure in a
test-method could leave a written file that would generate a false positive
result in/for later tests, the
tearDown
method is used to
make sure that if the expected files exist, they are deleted.
- The potential for false positives in this test is particularly worrisome,
since it would be possible for a bad test-run one day to leave an undeleted
test-result file that another test-run could pick up minutes, hours, or
days later. The fact that the files in question are being generated in
/tmp
would potentially help, but not prevent this from
happening.
- 37-51
- Fairly typical construction/derivation
- 49-69
- Tests the destruction of a
BaseDataObject
-derived class with
_Create
and _Update
implemented.
- 51
- Creates a new test-object, with an expected
IsNew
of
True
and IsDirty
of False
.
This is the default state for such a derived object.
- 52
- Deletes the newly-created object, which should fire the
_Create
method.
- 53-59
- Checks for the existence of the expected file that
_Create
should create
- 60-69
- With the exception of setting
IsDirty
on the test-object
(which should cause the _Update
method to fire on
destruction) and the file expected, the tests for a "dirty" object
With the exception of the unit-test for the Id
property, which
really can't usefully test the property anyway, the property- and method tests
are, I hope, what would be expected given the typical structure/pattern of such
tests from previous posts.