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
andTypeType
, for classic- and new-style-classes, respectively, plusInstanceType
andObjectType
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 consistentMemberTypes
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 basedict
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.
- 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).
No comments:
Post a Comment