Showing posts with label serialization. Show all posts
Showing posts with label serialization. Show all posts

Tuesday, March 14, 2017

Serialization: JSON (for now) [3]

Today's post is almost entirely code, after a very brief run-down of what a typical implementation-pattern of serialization functionality looks like.

IsJSONSerializable Subclassing Basics

Because Python is a dynamic programming language, abstraction requirements aren't checked, and so won't fail, until the code that is affected is executed. That is, if you define a class that derives from another class that has abstract properties or methods, but don't implement those abstract members in the derived class, nothing will happen until your code tries to make an instance of the derived class. For example if the following code were executed:

#!/usr/bin/env python
"""bad-class-demo.py: Demonstrates an abstract-class requirement that fails 
because those requirements aren't implemented."""

from serialization import IsJSONSerializable

class MySerializableClass( IsJSONSerializable ):
    pass

instance = MySerializableClass()
It yields this:
Traceback (most recent call last):
  File "bad-class-demo.py", line 10, in <>module>
    instance = MySerializableClass()
TypeError: Can't instantiate abstract class 
  MySerializableClass with abstract methods 
  GetSerializationDict
As soon as the required method exists, even if there is no real implementation, that error will go away, but this implementation is still not complete.

The bare minimum of what's necessary to successfully implement a complete subclass of IsJSONSerializable boils down to:

  • import the IsJSONSerializable abstract class in whatever fashion makes sense in the context of the code;
  • Define a class that is derived from IsJSONSerializable;
  • Make sure that the defined class provides a non-empty _jsonPublicFields class-attribute;
  • Make sure to call IsJSONSerializable.__init__ in the __init__ of the defined class (this is not technically necessary at this point, since there isn't anything happening in IsJSONSerializable.__init__ that, for example, sets default properties, but it's a good habit to get into, I think);
  • Implement the GetSerializationDict method — the stub-code below feels like a good starting-point to me, but you may prefer to take a different approach;
  • Implement the FromDict class-method; and
  • Register the defined class as JSON-loadable with [ClassName].RegisterLoadable().
All together, that looks like this:
from serialization import IsJSONSerializable

class JSONSerializableStub( IsJSONSerializable ):

  _jsonPublicFields = ( 'FieldName',  )

  def __init__( self ):
    IsJSONSerializable.__init__( self )
    # TODO: Whatever other initialization needs to happen

  def GetSerializationDict( self, sanitize=False ):
      # TODO: Actually implement code that reads the instance state 
      # and drops it into the "result" dict
      result = {}
      if sanitize:
        return self.SanitizeDict( result )
      return result

  @classmethod
  def FromDict( cls, data={}, **properties ):
    # Merge the properties and the data
    data.update( properties )
    # Create the instance to be returned
    result = cls()
    # TODO: Populate the instance with state-data from "data"
    # Return the instance
    return result

JSONSerializableStub.RegisterLoadable()

A Deeper Model

I'm going to build out some classes that model a hypothetical application that uses JSON-serialized files to store configuration-data. That Application has both simple configuration-values (strings only in this case, but numbers and booleans would also be feasible) and some complex values that are represented by instances of classes aggregated into the core Application structure. The example configuration-file that I'll be starting with looks like this:

{
   "Name":"Application",
   "UUID":"00000000-0000-0000-0000-000000000000",
   "Authentication":[
      {
         "Datastore":"ApplicationReader"
         "Type":"Database",
      },
      {
         "GUID":"The GUID of the provider",
         "Server":"ldap.company.com",
         "Type":"LDAP",
         "Name":"The name of the LDAP configuration",
         "NameFilter":"(&(objectClass=user)(objectCategory=Person))",
         "SearchBase":"DC=server,DC=company,DC=com"
      }
   ],
   "Datastores":{
      "ApplicationReader":{
         "Database":"ApplicationDatabase",
         "Host":"dbserver.company.com",
         "Password":"ApplicationReadOnlyUserPassword",
         "Type":"MySQL",
         "User":"ApplicationReadOnlyUser"
      },
      "ApplicationWriter":{
         "Database":"ApplicationDatabase",
         "Host":"dbserver.company.com",
         "Password":"ApplicationWriterUserPassword",
         "Type":"MySQL",
         "User":"ApplicationWriterUser"
      },
      "ExternalReadOnly":{
         "Database":"ExternalDatabase",
         "Host":"dbserver.external.com",
         "Password":"ExternalReadOnlyUserPassword",
         "Type":"ODBC",
         "User":"ExternalReadOnlyUser"
      }
   }
}

The entire JSON-object relates to a given Application instance, and its Authentication and Datastores properties to collections of instances of related classes whose relationship looks like this:

For simplicity, none of these classes have any methods, though I will probably build some out for various puropses as I work through the example, but I won't update this class-digram as I do so. Also, I won't go through my full template-based development cycle, since this example is pretty much throw-away code, only usable for demonstration purposes.

I'm going to demonstrate all of the functionality that IsJSONSerializable provides on this structure as follows:

  1. Creating a complete instance of the Application class by reading it from the JSON configuration file above, using Application.FromJSON;
  2. Showing that the entire application-structure is present by printing it (using a custom __str__ method that will render instances as an XML-like string);
  3. Show that json.dumps executes against the nested-object instance as needed to serialize the entire object to JSON;
  4. Show that the SanitizedJSON of the object strips out the fields that aren't listed in the class' _jsonPublicFields attribute;
  5. Show that json.dump also executes as needed agains the nested-object structure by writing a file, then showing it's written JSON structure;
  6. Use that output to unserialize an instance with json.load from the file; and
  7. Do te same with the content of the file read in as a string, and processed with json.loads.
The code for all that is pretty simple:
#!/usr/bin/env python
"""app-demo.py: Demonstrates the functionality made available by 
IsJSONSerializable."""

import json
import os

from AppClasses import *

#------------------------------------------------------------------------------#
# Create an Application instance from the example.json file                    #
#------------------------------------------------------------------------------#
fp = open( 'example.json', 'r' )
appFromGenericFile = Application.FromJSON( fp.read() )
fp.close()

#------------------------------------------------------------------------------#
# Show that the entire Application structure has been loaded by printing it    #
#------------------------------------------------------------------------------#
print ( '# -- Printing application structure ' ).ljust( 79, '-' ) + '#'
print appFromGenericFile

#------------------------------------------------------------------------------#
# Show that json.dumps works as expected                                       #
#------------------------------------------------------------------------------#
print '\n' + ( '# -- Printing json.dumps results ' ).ljust( 79, '-' ) + '#'
print json.dumps( appFromGenericFile, indent=2 )

#------------------------------------------------------------------------------#
# Show that SanitizedJSON works as expected                                    #
#------------------------------------------------------------------------------#
print '\n' + ( '# -- appFromGenericFile.SanitizedJSON ' ).ljust( 79, '-' ) + '#'
print appFromGenericFile.SanitizedJSON

#------------------------------------------------------------------------------#
# Show that json.dump works as expected                                        #
#------------------------------------------------------------------------------#
fp = 'aFGF.json'
print '\n' + ( '# -- Printing json.dump results to %s ' % fp ).ljust( 79, '-' ) + '#'
ofp = open( fp, 'w' )
json.dump( appFromGenericFile, ofp, indent=2 )
ofp.close()
ifp = open( fp, 'r' )
for line in ifp.read().split( '\n' ):
    print line
ifp.close()
# os.unlink( fp )

#------------------------------------------------------------------------------#
# Show that json.load works as expected                                        #
#------------------------------------------------------------------------------#
print '\n' + ( '# -- Printing json.load results from %s ' % fp ).ljust( 79, '-' ) + '#'
ifp = open( fp, 'r' )
secondApp = json.load( ifp )
ifp.close()
print secondApp

#------------------------------------------------------------------------------#
# Show that json.loads works as expected                                        #
#------------------------------------------------------------------------------#
print '\n' + ( '# -- Printing json.loads results from %s ' % fp ).ljust( 79, '-' ) + '#'
ifp = open( fp, 'r' )
rawJSON = ifp.read()
ifp.close()
thirdApp = json.loads( rawJSON )
print thirdApp
The implementation of the relevant classes:
#!/usr/bin/env python
"""AppClasses.py: The classes for demonstrating the functionality made 
available by IsJSONSerializable."""

import abc
import json
import uuid

import os, sys

from serialization import IsJSONSerializable

__all__ = []

class Application( IsJSONSerializable, object ):

  _jsonPublicFields = ( 'Authentication', 'Datastores', 'Name' )

  def __init__( self ):
    IsJSONSerializable.__init__( self )
    self.Authentication = []
    self.Datastores = {}
    self.Name = None
    self.UUID = str( uuid.uuid4() )

  def GetSerializationDict( self, sanitize=False ):
      # TODO: Actually implement code that reads the instance state 
      # and returns a dictionary representation of all of it.
      result = {
        'Authentication':[ 
          authenticator.GetSerializationDict( sanitize ) 
          for authenticator in self.Authentication
          ],
        'Datastores':dict(
          [
            ( dsname, self.Datastores[ dsname ].GetSerializationDict( sanitize ) )
            for dsname in self.Datastores
          ]
        ),
        'Name':self.Name,
        'UUID':self.UUID,
      }
      if sanitize:
        return self.SanitizeDict( result )
      return result

  def __str__( self ):
    result = '<Application uuid="%s" name="%s"' % ( self.UUID, self.Name )
    if self.Authentication or self.Datastores:
      result += '>\n'
      if self.Authentication:
        result += '  <Authentication>\n'
        for authenticator in self.Authentication:
          result += '    %s\n' % authenticator
        result += '  </Authentication>\n'
      if self.Datastores:
        result += '  <Datastores>\n'
        for datastore in self.Datastores:
          result += '    %s\n' % self.Datastores[ datastore ]
        result += '  <Datastores>\n'
      result += '</Application>'
    else:
      result += ' />'
    return result

  @classmethod
  def FromDict( cls, data={}, **properties ):
    # Merge the properties and the data
    data.update( properties )
    # Create the instance to be returned
    result = cls()
    # Populate the instance
    result.Name = data[ 'Name' ]
    result.UUID = data[ 'UUID' ]
    authenticators = data.get( 'Authentication' )
    if authenticators:
      for authenticator in authenticators:
        if authenticator[ 'Type' ] == 'Database':
          result.Authentication.append( 
            DatabaseAuthenticator.FromDict( authenticator )
          )
        if authenticator[ 'Type' ] == 'LDAP':
          result.Authentication.append( 
            LDAPAuthenticator.FromDict( authenticator )
          )
    datastores = data.get( 'Datastores' )
    if datastores:
      for dsname in datastores:
        datastore = datastores[ dsname ]
        if datastore[ 'Type' ] == 'MySQL':
          result.Datastores[ dsname ] = MySQLDatastore.FromDict( datastore )
        elif datastore[ 'Type' ] == 'ODBC':
          result.Datastores[ dsname ] = ODBCDatastore.FromDict( datastore )
    # Return the instance
    return result

Application.RegisterLoadable()

__all__.append( 'Application' )

class BaseAuthenticator( IsJSONSerializable, object ):
    __metaclass__ = abc.ABCMeta

    def __init__( self ):
        if self.__class__ == BaseAuthenticator:
            raise NotImplementedError( 'BaseAuthenticator is '
                'intended to be an abstract class, NOT to be '
                'instantiated.' )
        self.Type = None

__all__.append( 'BaseAuthenticator' )

class BaseDatastore( object ):

  __metaclass__ = abc.ABCMeta

  _jsonPublicFields = ( 'Database', )

  def __init__( self ):
    if self.__class__ == BaseDatastore:
      raise NotImplementedError( 'BaseDatastore is '
        'intended to be an abstract class, NOT to be '
        'instantiated.' )
    self.Database = None
    self.Host = None
    self.Password = None
    self.Type = None
    self.User = None

  def __str__( self ):
    return ( '<%s database="%s" host="%s" password="%s" '
      'type="%s" user="%s" />' % ( self.__class__.__name__, self.Database, 
      self.Host, self.Password, self.Type, self.User ) )

  def GetSerializationDict( self, sanitize=False ):
    result = {
        'Database':self.Database,
        'Host':self.Host,
        'Password':self.Password,
        'Type':self.Type,
        'User':self.User,
        }
    if sanitize:
      return self.SanitizeDict( result )
    return result

  @classmethod
  def FromDict( cls, data={}, **properties ):
    # Merge the properties and the data
    data.update( properties )
    result = cls()
    result.Database = data[ 'Database' ]
    result.Host = data[ 'Host' ]
    result.Password = data[ 'Password' ]
    result.Type = data[ 'Type' ]
    result.User = data[ 'User' ]
    return result

__all__.append( 'BaseDatastore' )

class DatabaseAuthenticator( BaseAuthenticator, object ):

  _jsonPublicFields = ( 'Datastore', 'Type' )

  def __init__( self ):
    BaseAuthenticator.__init__( self )
    self.Datastore = None
    self.Type = 'Database'

  def GetSerializationDict( self, sanitize=False ):
    result = {
        'Datastore':self.Datastore,
        'Type':self.Type,
        }
    if sanitize:
      return self.SanitizeDict( result )
    return result

  def __str__( self ):
    return ( '<%s datastore="%s" type="%s"  />' % ( 
        self.__class__.__name__, self.Datastore, self.Type ) )

  @classmethod
  def FromDict( cls, data={}, **properties ):
    # Merge the properties and the data
    data.update( properties )
    result = cls()
    result.Datastore = data[ 'Datastore' ]
    result.Type = 'Database'
    return result

DatabaseAuthenticator.RegisterLoadable()

__all__.append( 'DatabaseAuthenticator' )

class LDAPAuthenticator( BaseAuthenticator, object ):

  _jsonPublicFields = ( 'Name', 'Type' )

  def __init__( self ):
    BaseAuthenticator.__init__( self )
    self.GUID = None
    self.Server = None
    self.Type = 'LDAP'
    self.Name = None
    self.NameFilter = None
    self.SearchBase = None

  def GetSerializationDict( self, sanitize=False ):
    result = {
        'GUID':self.GUID,
        'Server':self.Server,
        'Type':self.Type,
        'Name':self.Name,
        'NameFilter':self.NameFilter,
        'SearchBase':self.SearchBase,
        }
    if sanitize:
      return self.SanitizeDict( result )
    return result

  def __str__( self ):
    return ( '<%s guid="%s" server="%s" type="%s" '
      'name="%s" namefilter="%s" searchbase="%s" />' % ( 
        self.__class__.__name__, self.GUID, self.Server, self.Type, 
        self.Name, self.NameFilter, self.SearchBase ) )

  @classmethod
  def FromDict( cls, data={}, **properties ):
    # Merge the properties and the data
    data.update( properties )
    result = cls()
    result.GUID = data[ 'GUID' ]
    result.Server = data[ 'Server' ]
    result.Type = 'LDAP'
    result.Name = data[ 'Name' ]
    result.NameFilter = data[ 'NameFilter' ]
    result.SearchBase = data[ 'SearchBase' ]
    return result

LDAPAuthenticator.RegisterLoadable()

__all__.append( 'LDAPAuthenticator' )

class MySQLDatastore( BaseDatastore, IsJSONSerializable, object ):

  def __init__( self ):
    BaseDatastore.__init__( self )
    IsJSONSerializable.__init__( self )

MySQLDatastore.RegisterLoadable()

__all__.append( 'MySQLDatastore' )

class ODBCDatastore( BaseDatastore, IsJSONSerializable, object ):

  def __init__( self ):
    BaseDatastore.__init__( self )
    IsJSONSerializable.__init__( self )

ODBCDatastore.RegisterLoadable()

__all__.append( 'ODBCDatastore' )

When this code is run, the _dumps call raises the warning:
app-demo.py:27: UnsanitizedJSONWarning: 
Application is an instance derived from IsJSONSerializable, 
and has "sanitized" JSON available in its SanitizedJSON property.
  print json.dumps( appFromGenericFile, indent=2 )
and generates the following output:
# -- Printing application structure -------------------------------------------#
<Application uuid="00000000-0000-0000-0000-000000000000" name="Application">
  <Authentication>
    <DatabaseAuthenticator datastore="ApplicationReader" type="Database"  />
    <LDAPAuthenticator guid="The GUID of the provider" server="ldap.company.com" type="LDAP" name="The name of the LDAP configuration" namefilter="(&(objectClass=user)(objectCategory=Person))" searchbase="DC=server,DC=company,DC=com" />
  </Authentication>
  <Datastores>
    <MySQLDatastore database="ApplicationDatabase" host="dbserver.company.com" password="ApplicationReadOnlyUserPassword" type="MySQL" user="ApplicationReadOnlyUser" />
    <MySQLDatastore database="ApplicationDatabase" host="dbserver.company.com" password="ApplicationWriterUserPassword" type="MySQL" user="ApplicationWriterUser" />
    <ODBCDatastore database="ExternalDatabase" host="dbserver.external.com" password="ExternalReadOnlyUserPassword" type="ODBC" user="ExternalReadOnlyUser" />
  <Datastores>
</Application>

# -- Printing json.dumps results ----------------------------------------------#
{
  "Datastores": {
    "ApplicationReader": {
      "Host": "dbserver.company.com", 
      "Password": "ApplicationReadOnlyUserPassword", 
      "Type": "MySQL", 
      "User": "ApplicationReadOnlyUser", 
      "Database": "ApplicationDatabase"
    }, 
    "ApplicationWriter": {
      "Host": "dbserver.company.com", 
      "Password": "ApplicationWriterUserPassword", 
      "Type": "MySQL", 
      "User": "ApplicationWriterUser", 
      "Database": "ApplicationDatabase"
    }, 
    "ExternalReadOnly": {
      "Host": "dbserver.external.com", 
      "Password": "ExternalReadOnlyUserPassword", 
      "Type": "ODBC", 
      "User": "ExternalReadOnlyUser", 
      "Database": "ExternalDatabase"
    }
  }, 
  "Authentication": [
    {
      "Datastore": "ApplicationReader", 
      "Type": "Database"
    }, 
    {
      "Name": "The name of the LDAP configuration", 
      "Server": "ldap.company.com", 
      "NameFilter": "(&(objectClass=user)(objectCategory=Person))", 
      "SearchBase": "DC=server,DC=company,DC=com", 
      "GUID": "The GUID of the provider", 
      "Type": "LDAP"
    }
  ], 
  "__namespace": "AppClasses.Application", 
  "Name": "Application", 
  "UUID": "00000000-0000-0000-0000-000000000000"
}

# -- appFromGenericFile.SanitizedJSON -----------------------------------------#
{
  "Datastores": {
    "ApplicationReader": {
      "Database": "ApplicationDatabase"
    }, 
    "ApplicationWriter": {
      "Database": "ApplicationDatabase"
    }, 
    "ExternalReadOnly": {
      "Database": "ExternalDatabase"
    }
  }, 
  "Authentication": [
    {
      "Datastore": "ApplicationReader", 
      "Type": "Database"
    }, 
    {
      "Type": "LDAP", 
      "Name": "The name of the LDAP configuration"
    }
  ], 
  "Name": "Application"
}

# -- Printing json.dump results to aFGF.json ----------------------------------#
{
  "Datastores": {
    "ApplicationReader": {
      "Host": "dbserver.company.com", 
      "Password": "ApplicationReadOnlyUserPassword", 
      "Type": "MySQL", 
      "User": "ApplicationReadOnlyUser", 
      "Database": "ApplicationDatabase"
    }, 
    "ApplicationWriter": {
      "Host": "dbserver.company.com", 
      "Password": "ApplicationWriterUserPassword", 
      "Type": "MySQL", 
      "User": "ApplicationWriterUser", 
      "Database": "ApplicationDatabase"
    }, 
    "ExternalReadOnly": {
      "Host": "dbserver.external.com", 
      "Password": "ExternalReadOnlyUserPassword", 
      "Type": "ODBC", 
      "User": "ExternalReadOnlyUser", 
      "Database": "ExternalDatabase"
    }
  }, 
  "Authentication": [
    {
      "Datastore": "ApplicationReader", 
      "Type": "Database"
    }, 
    {
      "Name": "The name of the LDAP configuration", 
      "Server": "ldap.company.com", 
      "NameFilter": "(&(objectClass=user)(objectCategory=Person))", 
      "SearchBase": "DC=server,DC=company,DC=com", 
      "GUID": "The GUID of the provider", 
      "Type": "LDAP"
    }
  ], 
  "__namespace": "AppClasses.Application", 
  "Name": "Application", 
  "UUID": "00000000-0000-0000-0000-000000000000"
}

# -- Printing json.load results from aFGF.json --------------------------------#
<Application uuid="00000000-0000-0000-0000-000000000000" name="Application">
  <Authentication>
    <DatabaseAuthenticator datastore="ApplicationReader" type="Database"  />
    <LDAPAuthenticator guid="The GUID of the provider" server="ldap.company.com" type="LDAP" name="The name of the LDAP configuration" namefilter="(&(objectClass=user)(objectCategory=Person))" searchbase="DC=server,DC=company,DC=com" />
  </Authentication>
  <Datastores>
    <MySQLDatastore database="ApplicationDatabase" host="dbserver.company.com" password="ApplicationReadOnlyUserPassword" type="MySQL" user="ApplicationReadOnlyUser" />
    <MySQLDatastore database="ApplicationDatabase" host="dbserver.company.com" password="ApplicationWriterUserPassword" type="MySQL" user="ApplicationWriterUser" />
    <ODBCDatastore database="ExternalDatabase" host="dbserver.external.com" password="ExternalReadOnlyUserPassword" type="ODBC" user="ExternalReadOnlyUser" />
  <Datastores>
</Application>

# -- Printing json.loads results from aFGF.json -------------------------------#
<Application uuid="00000000-0000-0000-0000-000000000000" name="Application">
  <Authentication>
    <DatabaseAuthenticator datastore="ApplicationReader" type="Database"  />
    <LDAPAuthenticator guid="The GUID of the provider" server="ldap.company.com" type="LDAP" name="The name of the LDAP configuration" namefilter="(&(objectClass=user)(objectCategory=Person))" searchbase="DC=server,DC=company,DC=com" />
  </Authentication>
  <Datastores>
    <MySQLDatastore database="ApplicationDatabase" host="dbserver.company.com" password="ApplicationReadOnlyUserPassword" type="MySQL" user="ApplicationReadOnlyUser" />
    <MySQLDatastore database="ApplicationDatabase" host="dbserver.company.com" password="ApplicationWriterUserPassword" type="MySQL" user="ApplicationWriterUser" />
    <ODBCDatastore database="ExternalDatabase" host="dbserver.external.com" password="ExternalReadOnlyUserPassword" type="ODBC" user="ExternalReadOnlyUser" />
  <Datastores>
</Application>

All of the code above, plus a stripped-down copy of serialization.py is in the download-package for today's post — Everything needed to run the code is all there.



This sort of testing — writing a program to show that pieces of functionality are doing what they're supposed to — is something that most developers do from time to time at least, I expect. It's often just easier to work through issues that arise when that sort of test-code is run. That said, it's not all that thorough a test-process, so starting in my next post, I'll dig in to some unit-testing thoughts and processes.

Thursday, March 9, 2017

Serialization: JSON (for now) [2]

With today's post, I plan to wrap up the serialization module's members. I'd originally planned to also show a simple example of how to implement the serialization functionality they provide, but after the post had grown to the length it ended up at, and since I'd planned on showing a deeper example, it feels to me like it makes more sense to roll the basic implementation guidelines into the deeper look.

The Other Serialization Class

There's just one more class in the serialization module that needs to be shown, but it's the one that will probably be used most often: IsJSONSerializable. IsJSONSerializable is the abstract class that concrete classes will inherit from in order to access the SanitizedJSON it provides, as well as to be able to be registered for automatic json-module handling for that module's dump* and load* functions. The class-diagram for IsJSONSerializable looks like this:

It's also important to remember that IsJSONSerializable derives from (kind of implements) HasSerializationDict. It doesn't actually implement any of the functionality required by HasSerializationDict, but it does pass that requirement on to the concrete classes derived from it. HasSerializationDict is a pretty small interface:

The IsJSONSerializable Abstract Class

There isn't a lot about IsJSONSerializable that I haven't already discussed, and none of it that I think is relevant before I show the actual code, so I'll just do that:

@describe.InitClass()
class IsJSONSerializable( HasSerializationDict, object ):
    """
Provides baseline functionality, interface requirements and type-identity for 
objects whose state-data can be serialized to and unserialized from JSON."""

    #-----------------------------------#
    # Abstraction through abc.ABCMeta   #
    #-----------------------------------#
    __metaclass__ = abc.ABCMeta

    #-----------------------------------#
    # Class attributes (and instance-   #
    # attribute default values)         #
    #-----------------------------------#

    _decoratedJSON = {}
    _jsonPublicFields = ()
    _registeredLoadables = {}

    #-----------------------------------#
    # Instance property-getter methods  #
    #-----------------------------------#

    @describe.AttachDocumentation()
    def _GetPythonNamespace( self ):
        """
Gets the Python namespace of the class that the instance is an instance of."""
        return '%s.%s' % ( self.__class__.__module__, 
            self.__class__.__name__ )

    @describe.AttachDocumentation()
    def _GetSanitizedJSON( self ):
        """
Gets a minimized JSON representation of the instance containing only those 
fields that are deemed safe for transmission over a network"""
        if not self.__class__._jsonPublicFields:
            raise AttributeError( '%s.SanitizedJSON cannot be retrieved, '
                'because %s has not specified any public JSON fields (fields '
                'allowed to be represented in a JSON dump). Correct this by '
                'populating the _jsonPublicFields class-attribute of %s' % ( 
                    self.__class__.__name__, self.__class__.__name__, 
                    self.__class__.__name__
                )
            )
        return json.dumps( self.GetSerializationDict( True ) )

    #-----------------------------------#
    # Instance property-setter methods  #
    #-----------------------------------#

    #-----------------------------------#
    # Instance property-deleter methods #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Properties (abstract OR  #
    # concrete!)                        #
    #-----------------------------------#

    PythonNamespace = describe.makeProperty( 
        _GetPythonNamespace, None, None, 
        'the fully-qualified Python namespace of the instance\'s class',
        str, unicode
    )
    SanitizedJSON = describe.makeProperty( 
        _GetSanitizedJSON, None, None, 
        'a minimized JSON representation of the instance containing only '
        'those fields that are deemed safe for transmission over a network',
        unicode, str
    )

    #-----------------------------------#
    # Instance Initializer              #
    #-----------------------------------#
    @describe.AttachDocumentation()
    @describe.todo( 'Document __init__' )
    @describe.todo( 'Implement __init__' )
    def __init__( self ):
        """
Instance initializer"""
        # IsJSONSerializable is intended to be an abstract class,
        # and is NOT intended to be instantiated. Alter at your own risk!
        if self.__class__ == IsJSONSerializable:
            raise NotImplementedError( 'IsJSONSerializable is '
                'intended to be an abstract class, NOT to be instantiated.' )
        # Call parent initializers, if applicable.
        HasSerializationDict.__init__( self )
        # Set default instance property-values with _Del... methods as needed.
        # Set instance property values from arguments if applicable.
        # Other set-up

    #-----------------------------------#
    # Instance Garbage Collection       #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Methods                  #
    #-----------------------------------#

    @describe.AttachDocumentation()
    @describe.argument( 'dictIn', 
        'the dictionary to sanitize',
        str, unicode
    )
    @describe.keywordargs( 'additional values to add to dictIn' )
    def SanitizeDict( self, dictIn={}, **dictItems ):
        """
Returns a copy of the supplied dict, with any fields that aren't members of 
the class' _jsonPublicFields attribute removed."""
        if dictIn and not isinstance( dictIn, dict ):
            raise TypeError( '%s.SanitizeDict expects a dictionary of '
                'values to sanitize, or None for its dictIn argument, but '
                'was passed "%s" (%s)' % ( self.__class__.__name__, 
                    dictIn, type( dictIn ).__name__ ) )
        dictIn.update( dictItems )
        return dict(
            [
                ( key, dictIn[ key ] ) for key in dictIn 
                if key in self.__class__._jsonPublicFields
            ]
        )

    #-----------------------------------#
    # Class Methods                     #
    #-----------------------------------#

    @classmethod
    @describe.AttachDocumentation()
    @describe.argument( 'jsonData', 
        'the JSON representation of the object to create and return',
        str, unicode
    )
    @describe.raises( TypeError, 'if passed a jsonData argument that is not a '
        'str or unicode value' )
    def FromJSON( cls, jsonData ):
        """
Creates and returns an instance of the class whose state-data is populated with 
the values from the supplied JSON serialization-data"""
        if type( jsonData ) not in ( str, unicode ):
            raise TypeError( '%s.FromJSON expectes a str or unicode JSON '
                'construct, but was passed "%s" (%s)' % ( 
                cls.__name__, jsonData, type( jsonData ).__name__ )
            )
        return cls.FromDict( json.loads( jsonData ) )

    @classmethod
    def RegisterLoadable( cls ):
        """
Registers the class as a JSON-serializable type with IsJSONSerializable."""
        namespace = '%s.%s' % ( cls.__module__, cls.__name__ )
        if IsJSONSerializable._registeredLoadables.get( namespace ):
            raise RuntimeError( 'The %s class cannot be registered as JSON-'
                'loadable using the %s namespace: %s has already been '
                'registered as a JSON-loadable there' % ( cls.__name__, 
                namespace, 
                IsJSONSerializable._registeredLoadables[ namespace ].__name__ )
            )
        IsJSONSerializable._registeredLoadables[ namespace ] = cls

    #-----------------------------------#
    # Static Class Methods              #
    #-----------------------------------#

    @staticmethod
    @describe.argument( 'origfunc', 
        '[json.dump] The original function to wrap IsJSONSerializable checking'
        'around'
    )
    @describe.raises( RuntimeError, 'if not passed json.dump' )
    @describe.returns( 'The replacement function for json.dump' )
    def wrapjsondump( origfunc ):
        """
Wraps checking for IsJSONSerializable-derived classes around the standard 
json.dump function. Note that this dumps *ALL* fields, so output is *NOT* 
sanitized for over-the-wire transit!"""
        if IsJSONSerializable._decoratedJSON.get( origfunc ):
            return IsJSONSerializable._decoratedJSON[ origfunc ]
        if origfunc != json.dump:
            raise RuntimeError( 'wrapjsondump expects json.dump as the '
                'function to decorate,but was passed %s' % ( origfunc ) )
        def _dump( obj, fp, skipkeys=False, ensure_ascii=True, 
            check_circular=True, allow_nan=True, cls=None, indent=None, 
            separators=None, encoding='utf-8', default=None, sort_keys=False, 
            **kw ):
            if isinstance( obj, IsJSONSerializable ):
                objNS = obj.PythonNamespace
                obj = obj.GetSerializationDict()
                obj[ '__namespace' ] = objNS
            return origfunc( obj, fp, skipkeys, ensure_ascii, check_circular, 
                allow_nan, cls, indent, separators, encoding, default, 
                sort_keys, **kw )
        IsJSONSerializable._decoratedJSON[ origfunc ] = _dump
        return _dump

    @staticmethod
    @describe.argument( 'origfunc', 
        '[json.dumps] The original function to wrap IsJSONSerializable checking'
        'around'
    )
    @describe.raises( RuntimeError, 'if not passed json.dumps' )
    @describe.returns( 'The replacement function for json.dumps' )
    def wrapjsondumps( origfunc ):
        """
Wraps checking for IsJSONSerializable-derived classes around the standard 
json.dumps function. Note that this dumps *ALL* fields, so output is *NOT* 
sanitized for over-the-wire transit!"""
        if IsJSONSerializable._decoratedJSON.get( origfunc ):
            return IsJSONSerializable._decoratedJSON[ origfunc ]
        if origfunc != json.dumps:
            raise RuntimeError( 'wrapjsondump expects json.dump as the '
                'function to decorate,but was passed %s' % ( origfunc ) )
        def _dumps( obj, skipkeys=False, ensure_ascii=True, 
            check_circular=True, allow_nan=True, cls=None, indent=None, 
            separators=None, encoding='utf-8', default=None, sort_keys=False, 
            **kw ):
            if isinstance( obj, IsJSONSerializable ):
                warnings.warn( '%s is an instance derived from '
                    'IsJSONSerializable, and has "sanitized" JSON available '
                    'in its SanitizedJSON property..' % 
                        ( obj.__class__.__name__ ), 
                        UnsanitizedJSONWarning, stacklevel=2
                    )
                objNS = obj.PythonNamespace
                obj = obj.GetSerializationDict()
                obj[ '__namespace' ] = objNS
            return origfunc( obj, skipkeys, ensure_ascii, check_circular, 
                allow_nan, cls, indent, separators, encoding, default, 
                sort_keys, **kw )
        IsJSONSerializable._decoratedJSON[ origfunc ] = _dumps
        return _dumps

    @staticmethod
    @describe.argument( 'origfunc', 
        '[json.load] The original function to wrap IsJSONSerializable checking'
        'around'
    )
    @describe.raises( RuntimeError, 'if not passed json.load' )
    @describe.returns( 'The replacement function for json.load' )
    def wrapjsonload( origfunc ):
        """
Replaces json.load with a function that hands processing off to a subclass of 
IsJSONSerializable for unserialization of the JSON data into an instance of the 
class when applicable"""
        if origfunc != json.load:
            raise RuntimeError( 'wrapjsonload expects json.load as the '
                'function to decorate, but was passed %s' % ( origfunc ) )
        if IsJSONSerializable._decoratedJSON.get( origfunc ):
            return IsJSONSerializable._decoratedJSON[ origfunc ]
        def _load( *args, **kw ):
            baseDict = origfunc( *args, **kw )
            try:
                objNS = baseDict.get( '__namespace' )
            except AttributeError:
                objNS = None
            if objNS:
                if type( baseDict ) != dict:
                    raise ValueError( 'Decorated override of json.loads '
                        'expected a dict value to convert to an instance of '
                        'IsJSONSerializable, but the supplied JSON evaluated '
                        'to "%s" (%s)' % ( 
                            baseDict, type( baseDict ).__name__
                        )
                    )
                objClass = IsJSONSerializable._registeredLoadables.get( objNS )
                if objClass:
                    return objClass.FromDict( baseDict )
                raise RuntimeError( 'decorated override of json.load could not '
                    'find a valid object-namespace (%s) to work with: %s' % ( 
                    objNS, args[ 0 ] ) )
            return baseDict
        IsJSONSerializable._decoratedJSON[ origfunc ] = _load
        return _load

    @staticmethod
    @describe.argument( 'origfunc', 
        '[json.loads] The original function to wrap IsJSONSerializable checking'
        'around'
    )
    @describe.raises( RuntimeError, 'if not passed json.loads' )
    @describe.returns( 'The replacement function for json.loads' )
    def wrapjsonloads( origfunc ):
        """
Replaces json.loads with a function that hands processing off to a subclass of 
IsJSONSerializable for unserialization of the JSON data into an instance of the 
class when applicable"""
        if origfunc != json.loads:
            raise RuntimeError( 'wrapjsonloads expects json.loads as the '
                'function to decorate,but was passed %s' % ( origfunc ) )
        if IsJSONSerializable._decoratedJSON.get( origfunc ):
            return IsJSONSerializable._decoratedJSON[ origfunc ]
        def _loads( *args, **kw ):
            baseDict = origfunc( *args, **kw )
            try:
                objNS = baseDict.get( '__namespace' )
            except AttributeError:
                objNS = None
            if objNS:
                if type( baseDict ) != dict:
                    raise ValueError( 'Decorated override of json.loads '
                        'expected a dict value to convert to an instance of '
                        'IsJSONSerializable, but the supplied JSON evaluated '
                        'to "%s" (%s)' % ( 
                            baseDict, type( baseDict ).__name__
                        )
                    )
                objClass = IsJSONSerializable._registeredLoadables.get( objNS )
                if objClass:
                    return objClass.FromDict( baseDict )
                raise RuntimeError( 'decorated override of json.loads could '
                    'not find a valid object-namespace (%s) to work with: %s' % 
                    ( objNS, args[ 0 ] )
                )
            return baseDict
        IsJSONSerializable._decoratedJSON[ origfunc ] = _loads
        return _loads

#---------------------------------------#
# Append to __all__                     #
#---------------------------------------#
__all__.append( 'IsJSONSerializable' )

From top to bottom, here are the pieces that I think are worth mentioning in a bit more detail...

The _jsonPublicFields class-attribute is inherited by derived classes, rather than being used in IsJSONSerializable itself. In those derived classes, it is used by the property-getter method for the SanitizedJSON property (_GetSanitizedJSON) to determine which of the class' properties/fields will be kept in the sanitized JSON output for instances of the class.

The _registeredLoadables class-attribute is where registered classes derived from IsJSONSerializable are kept track of. It shouldn't be altered by derived classes directly — all that needs to be done with it is done by the inherited RegisterLoadable class-method. I'll go a bit more into that when I show how to use IsJSONSerializable a bit later in this post.

_GetSanitizedJSON, the property-getter for the SanitizedJSON property, turned out to be a lot less complicated than I'd feared it would — But a good part of that, I suspect, is because all of the heavy lifting involved in sanitizing JSON output really has to happen at the level of the GetSerializationDict method required in derived classes, but not dealt with at all in IsJSONSerializable itself.

I had several false starts while I was trying to work out a solid mechanism for providing access to sanitized JSON output. This is, I think, the third or perhaps fourth run I've taken at it, but this feels like it should do what I want it to do, based on some of the preliminary code I've started writing for the deep dive post that will follow this one.

The SanitizeDict method provides a common mechanism that will exist across all instances derived from IsJSONSerializable to generate a sanitized dictionary, using the instance's class' _jsonPublicFields attribute to determine what can be safely sent across the wire. Although implementation of GetSerializationDict in derived classes can (and argubaly should) use SanitizeDict to filter down the generic GetSerializationDict results, there's no actual reason that implementations must do so... It's there as a convenient standard process, and will have been thoroughly tested, so there's an advantage to doing so, but it's not required.

All told, this entire process also ensures that a developer has to make a conscious decision to include a field in the sanitized JSON output — anything that isn't explicitly allowed is removed.

FromJSON uses the FromDict method (required by HasSerializationDict) of the derived class to create an instance of the class, populated with the data from the JSON, and returns it. Under normal circumstances, this will be called by the overridden json.load* functions from the last post, and they will know which class' FromJSON to call based on the __namespace provided in the JSON. That assumes that the __namespace is provided, of course, but any JSON generated by the overridden json.dump* functions will have them.

RegisterLoadable is the class-method that registers the class as available for the overridden json.load* functions. In use, it should be called from the derived class as soon as that class is defined. For example:

class MyClass( IsJSONSerializable ):
    # ... the definition of the class ...

MyClass.RegisterLoadable()

The last item of note with respect to IsJSONSerializable is the disposition of the json-function decorators from the last post. I decided to move those into IsJSONSerializable as static methods rather than keeping them as free-standing functions in the serialization module. I can't really completely explain why I chose to do that, though — most of it was a gut feeling that it'd be safer or maybe easier to manage if they didn't have to be imported one at a time from outside the module, though I couldn't think of a use-case where I'd want or need to do that that made any sense. Still, I have a lingering feeling that it could happen, so for now I'm going to leave them where I've put them.

Other Things That the serialization module does

It may be obvious to state this: When a module is imported, all the code in it that can be executed is executed. That means that a module can call functions to perform tasks that might be needed for the module to provide all of the functionality it's supposed to. It may be less obvious that when a member of a module is imported (like, say, only IsJSONSerializable from the serialization module) all the module's code is still executed.

Remember the IsJSONSerializable.wrapjson* static methods?

Those need to be fired off, assigning their replacement functions to all of the original json-module functions they are intended to replace, any time that IsJSONSerializable is in play. The serialization module does this by explicitly calling them (replacing the originals with the decorator-replacements) like this:

#-----------------------------------#
# Decoration/override of json.xxxxx #
#-----------------------------------#
json.dump = IsJSONSerializable.wrapjsondump( json.dump )
json.dumps = IsJSONSerializable.wrapjsondumps( json.dumps )
json.load = IsJSONSerializable.wrapjsonload( json.load )
json.loads = IsJSONSerializable.wrapjsonloads( json.loads )

I'll jump right in to a simple implementation of IsJSONSerializable (with all of the basic requirements and caveats) in my next post, as well as the promised deep dive into what a IsJSONSerializable implementation can actually do. In the meantime, here's the final code for the serialization module:

Tuesday, March 7, 2017

Serialization: JSON (for now) [1]

OK. Now that the JavaScript interlude is done, it's time to get back to the Python framework... I've gone back and forth a few times now on what to tackle next, and though I'd really like to start working on some code to handle parsing, creating, and working with markup elements, I feel that I'd be remiss if I didn't start addressing some of the outstanding items in my coding standards. Specifically:

  1. It should be thoroughly tested
Since that involves unit-testing — and I have some specific desires around enforcing testing-standards and code-coverage — I think I'd be better off working through that with a smaller set of classes. I started planning out a set of posts about a DAL next, but that grew too unwieldy to be able to keep the unit-testing follow-up short, sweet and to the point as well. After rooting around a bit in other parts of the framework that I had outlined, I decided to implement a serialization module. It didn't have any concrete classes, but I could create an example of one for demonstration of unit-testing once I got done with the core serialization logic, and there were other... interesting aspects... to play with.

My Goals for Object Serialization

Since a lot of the projects that I'm contemplating are web-applications of some sort, and JSON is popular in that context, I want to be able to define some common functionality that will allow objects' state to be easily serialized into and unserialized from JSON. Python also has its pickle module, which I'll eventually want to incorporate, I suspect, but JSON will do for now.

One thing that I very much dislike about Python's JSON implementations that I've seen thus far is that while the json module's functions do a good job of dealing with built-in Python types, the mechanism for making an arbitrary instance of a class JSON-serializable seems cumbersome, requiring (apparently) the creation of a dedicated JSONDecoder and JSONEncoder classes. Over and above that, I can easily imagine the need to be able to JSON-encode a given object in two distinct ways: One for public consumption over the web (making sure to sanitize out any sensitive data), and one for private use (allowing, if not mandating that sensitive data be present and accounted for). That means that there would be a minimum of two custom classes that would have to be defined for each JSON-serializable class. Some thought would also have to go into determining how to switch between those variants as well. Finally, that concrete class-requirement would make it difficult to associate as an inner class, which was where my first thoughts led me.

Ugh.

Instead, I'm going to define an abstract class that requires a JSON-serializable object to have a some required members that provide the serialization and unserialization functionality. My first thought was to require a ToJSON instance-method and a FromJSON class-method. Each of those would require an instance-level GetSerializationDict and class-scoped FromDict method at another level of abstraction (an interface that the abstract class inherits from).

Then I got to thinking... Serialization to and from JSON has at least two distinct uses that I can think of off the top of my head:

  • Serializing/unserializing sanitized data for transmission over a network — something that a lot of web-services do; and
  • Serializing/unserializing for local use — Perhaps for writing data to a local file or a record in a database, for instance.
The concern that this brought to mind might be expressed as:
What if an object needs to be serialized for both these cases? A secure variant and a public variant?
After considering some of the options, the approach I'm going to take is to provide a SanitizedJSON property on all classes derived from the abstract class. That will provide a bare-bones, already-minimized and sanitized JSON output for any instance of a derived class.

That means, however, that standard json.dump and json.dumps results have to be considered in the unsanitized data category. In the case of json.dump, since it writes to a file, that's likely not going to be a concern for any of the typical application structures and environments I've been exposed to. Output from json.dumps, though, might well be used to generate JSON data for unsafe purposes, so I'll want to raise some sort of warning when that happens — nothing that prevents it (or stops execution), but that will hopefully alert a developer that there is an available option if it's needed.

All that said, here's what I'm going to define and implement for the serialization module:

  • HasSerializationDict interface: Requires implementation of a GetSerializationDict instance-method and a FromDict class-method.
  • IsJSONSerializable abstract class (extends/implements HasSerializationDict): Implements a SanitizedJSON property that uses the GetSerializationDict method required by HasSerializationDict, and requires a FromJSON class-method.

There will be a few other items in the final structure, but those are the main points I think I need to cover for now. Chief among those other points: I still want to allow the standard json-module functions to be callable on IsJSONSerializable-derived objects without raising errors. I've played around with a few ideas to make that workable, and have something that I think will work well. I'll go into that in depth later, after writing out the interface and abstract class, because demonstrating that it works will require a fair chunk of code all by itself.

The First Two Serialization Classes

In previous posts, I started with the class-diagrams and interface specifics, then worked through the implementation. In this case, I'm going to start with the code, since a lot of the exploration I did to arrive at my final solution yielded fully- or near-fully implemented code.

The UnsanitizedJSONWarning Warning

At some point, once I get the json module functions' relationship with IsJSONSerializable worked out, I'll want to raise a warning if json.dumps is being called against an instance of a derived class. Python's warnings module provides the base functionality I need for that, and there are serveral Warning-derived classes already available. None of them really look like they're quite what I'd like to see when that warning situation occurs, though, so I'm going to create a custom Warning of my own:

#-----------------------------------#
# Defined exceptions.               #
#-----------------------------------#

@describe.InitClass()
class UnsanitizedJSONWarning( Warning ):
    """
A warning to be raised when json.dumps is used to generate 
potentially-unsanitized JSON output"""
    pass

__all__.append( 'UnsanitizedJSONWarning' )

The Warning class is derived from the standard Exception class. My experience has been that although I've created custom Exceptions with some frequency, I've never had to do anything more complex than this — usually if I feel the need to make a custom Exception, it's because I've determined that I need to be able to raise one and catch it elsewhere while allowing the except that catches it to differentiate between different exception-types. In this case, it's mostly because I'd like the warning that I'm going to surface to give some indication of what the warning's actually about. Something like this:

UnsanitizedJSONWarning: [class-name] has 
   "sanitized" JSON available in obj.SanitizedJSON.
This custom Warning will allow that.

The HasSerializationDict Interface

The first item to go over is the HasSerializationDict interface. The intention around it is to provide some (minimal) requirements for all classes whose instances can be serialized in pretty much any fashion, by requiring that those instances be capable of generating a dictionary representation of their state. Both JSON serialization (now) and pickle-based serialization (some time later, maybe) can use a dict data-structure as a common mid-point for serializing and unserializing objects, so that just seemed a logical starting-point. Since it's an interface (at least nominally), there's not much to the code:

@describe.InitClass()
class HasSerializationDict( object ):
    """
Provides interface requirements and type-identity for objects that are 
required to implement serialization dictionaries: a dict representation of the 
instance used as a process-step for serializing object state"""

    #-----------------------------------#
    # Abstraction through abc.ABCMeta   #
    #-----------------------------------#
    __metaclass__ = abc.ABCMeta

    #-----------------------------------#
    # Static interface attributes (and  #
    # default values?)                  #
    #-----------------------------------#

    #-----------------------------------#
    # Abstract Properties               #
    #-----------------------------------#

    #-----------------------------------#
    # Instance Initializer              #
    #-----------------------------------#
    @describe.AttachDocumentation()
    def __init__( self ):
        """
Instance initializer"""
        # HasSerializationDict is intended to be an interface,
        # and is NOT intended to be instantiated. Alter at your own risk!
        if self.__class__ == HasSerializationDict:
            raise NotImplementedError( 'HasSerializationDict is '
                'intended to be an interface, NOT to be instantiated.' )
        # Call parent initializers, if applicable.
        # Other set-up

    #-----------------------------------#
    # Abstract Instance Methods         #
    #-----------------------------------#

    @abc.abstractmethod
    @describe.AttachDocumentation()
    @describe.argument( 'sanitize',
        'indicates whether the returned dictionary should be "sanitized," '
        'the implementation of which is up to the derived class',
        bool
    )
    def GetSerializationDict( self, sanitize=False ):
        """
Returns a dict representation of the instance."""
        raise NotImplementedError( '%s.GetSerializationDict has not been '
            'implemented as required by HasSerializationDict' % 
            ( self.__class__.__name__ )
        )

    #-----------------------------------#
    # Abstract Class Methods            #
    #-----------------------------------#

    @classmethod
    @describe.AttachDocumentation()
    @describe.argument( 'data',
        'the state-data to be used to create the new instance',
        dict
    )
    @describe.keywordargs( 
        'keyword arguments representation of the state-data to be used to '
        'create the new instance. NOTE: If provided, these override any values '
        'provided in the data argument!'
    )
    @describe.raises( NotImplementedError, 
        'if called by a derived object that has not overridden the nominally-'
        'abstract method' )
    def FromDict( cls, data={}, **properties ):
        """
[Nominally-abstract class-method] Returns an instance of the class whose state-
data has been populated with the values provided in the data and/or properties 
supplied."""
        raise NotImplementedError( '%s.FromDict has not been implemented as '
            'required by HasSerializationDict' % ( cls.__name__ ) )

    #-----------------------------------#
    # Static Class Methods              #
    #-----------------------------------#

#---------------------------------------#
# Append to __all__                     #
#---------------------------------------#
__all__.append( 'HasSerializationDict' )

It's worth noting that the FromDict class-method is not decorated as an abstractmethod. Python 2.7 doesn't support decorating a method as both a classmethod and an abstractmethod. I'm not sure if Python 3.x will or not — I haven't looked — but both methods are built to raise a NotImplementedError in any event, so even if FromDict isn't implemented in a derived class, it will raise that error as soon as it's called.

That's something that should happen during unit-testing, and I'll show how I'm planning to deal with that once I start down the unit-testing code that I mentioned before.

Integrating the json Module Functions

I'll be honest: I struggled with how to try and make instances of IsJSONSerializable tie in nicely with the json-modules' dumping- and loading-functions. I poked around a lot of websites, read through a fair number of stackoverflow articles, and took a lot of fairly long walks to try and get some right-brain creativity to kick in. For a good, long while, the problem looked insurmountable.

Then, while reviewing the doc_metadata posts prior to their publication, I started wondering if I could apply a decorator to those functions. So I tried it, and it worked!

After a fair bit of tinkering with the idea, I ended up with four functions, one each to wrap around one of the json.* functions that I was concerned with. What I'm actually doing with them doesn't feel like a typical Python decorator to me, but (amusingly enough) does feel like an application of the Decorator design pattern. I'll go over each in as much detail as seems relevant...

def wrapjsondump( origfunc ):
    """
Wraps checking for IsJSONSerializable-derived classes around the standard 
json.dump function. Note that this dumps *ALL* fields, so output is *NOT* 
sanitized for over-the-wire transit!"""
    if IsJSONSerializable._decoratedJSON.get( origfunc ):
        return IsJSONSerializable._decoratedJSON[ origfunc ]
    if origfunc != json.dump:
        raise RuntimeError( 'wrapjsondump expects json.dump as the '
            'function to decorate,but was passed %s' % ( origfunc ) )
    def _dump( obj, fp, skipkeys=False, ensure_ascii=True, 
        check_circular=True, allow_nan=True, cls=None, indent=None, 
        separators=None, encoding='utf-8', default=None, sort_keys=False, 
        **kw ):
        if isinstance( obj, IsJSONSerializable ):
            objNS = obj.PythonNamespace
            obj = obj.GetSerializationDict()
            obj[ '__namespace' ] = objNS
        return origfunc( obj, fp, skipkeys, ensure_ascii, check_circular, 
            allow_nan, cls, indent, separators, encoding, default, 
            sort_keys, **kw )
    IsJSONSerializable._decoratedJSON[ origfunc ] = _dump
    return _dump

All of these decorator-functions accept an original function (origfunc), and a replacement function (_dump in this case). The original function persists inside the replacement function because of the way Python's closures work, leaving it accessible within the scope of the replacement, but able to be overridden outside that scope. Each of the replacement functions was written to use the same signature as the functions they replace.

A step-by-step breakdown of what happens may be useful. I'll use this function as the example, but the process is very similar with the other three:

  • Somewhere in some code, json.dump = wrapjsondump( json.dump ) is called;
    • The decorator checks to see if origfunc has already been decorated by looking up the replacement function in IsJSONSerializable._decoratedJSON. If it has been, the decorator immediately returns that found function.
      I'm not sure that this is doing exactly what I want/need, but until I get a chance to test it more thoroughly than I have at this point, I'm satisfied that it seems to be working.
      The decision to store the look-up in IsJSONSerializable was made based on the realization that it would be available any place that an instance that required the use of the decorated functions would exist — they'd have to be subclasses of IsJSONSerializable, after all.
    • A check is performed to make sure that the decorator is being applied to the appropriate original function.
    • The replacement function is defined (_dump in this case);
    • The replacement function is added to IsJSONSerializable._decoratedJSON, using the origfunc itself as the key.
    • The replacement function is returned.
    That replacement function over-writes json.dump (because the initial call that started all of this was
    json.dump = wrapjsondump( json.dump )
    From that point on, any call to json.dump will instead be handed off to the replacement _dump function.
  • Later, somewhere else, a call to json.dump is made, passing an object to be serialized:
    • Since json.dump has been replaced with _dump by the decorator, _dump is called instead:
      • The supplied object (obj) is checked, to see if it's an instance of IsJSONSerializable:
        • If it is, then a dict is built out, starting with the results of obj.GetSerializationDict(), adding a '__namespace' key to it, and the original object is replaced with the dict
        • The original function (json.dump) is called, passing the (possibly-modified) object, and
        • The results are returned.

In the final analysis, the only reason this approach works is because the original function that is being replaced is still (in fact, only) accessible inside the scope of the function it's being replaced with. If that sounds weird to you, you're not alone. I couldn't come up with any simpler way to explain it, and I'm not sure that I'm qualified to explain why it works without that explanation eventually devolving into mumbling about closures in functions.

The wrapjsondumps is very similar to wrapjsondump — not surprising, I think, since they perform the same basic function, just to different outputs:

def wrapjsondumps( origfunc ):
    """
Wraps checking for IsJSONSerializable-derived classes around the standard 
json.dumps function. Note that this dumps *ALL* fields, so output is *NOT* 
sanitized for over-the-wire transit!"""
    if IsJSONSerializable._decoratedJSON.get( origfunc ):
        return IsJSONSerializable._decoratedJSON[ origfunc ]
    if origfunc != json.dumps:
        raise RuntimeError( 'wrapjsondump expects json.dump as the '
            'function to decorate,but was passed %s' % ( origfunc ) )
    def _dumps( obj, skipkeys=False, ensure_ascii=True, 
        check_circular=True, allow_nan=True, cls=None, indent=None, 
        separators=None, encoding='utf-8', default=None, sort_keys=False, 
        **kw ):
        if isinstance( obj, IsJSONSerializable ):
            # TODO: Figure out how to generate an exception-like warning
            #       instead of printing this message
            warnings.warn( '%s is an instance derived from '
                'IsJSONSerializable, and has "sanitized" JSON available '
                'in its SanitizedJSON property..' % 
                    ( obj.__class__.__name__ ), 
                    UnsanitizedJSONWarning, stacklevel=2
                )
            objNS = obj.PythonNamespace
            obj = obj.GetSerializationDict()
            obj[ '__namespace' ] = objNS
        return origfunc( obj, skipkeys, ensure_ascii, check_circular, 
            allow_nan, cls, indent, separators, encoding, default, 
            sort_keys, **kw )
    IsJSONSerializable._decoratedJSON[ origfunc ] = _dumps
    return _dumps

The significant differences are the signature of the replacement function (it has to match the signature of the original json.dump function), and the warning that gets raised if obj is an instance of IsJSONSerializable. That's this chunk of code:


if isinstance( obj, IsJSONSerializable ):
    # TODO: Figure out how to generate an exception-like warning
    #       instead of printing this message
    warnings.warn( '%s is an instance derived from '
        'IsJSONSerializable, and has "sanitized" JSON available '
        'in its SanitizedJSON property..' % 
            ( obj.__class__.__name__ ), 
            UnsanitizedJSONWarning, stacklevel=2
        )

The load-related functions, though they follow the same decorator-pattern as the dump-centric ones, has a very different internal process. In order for a load to be able to create an actual instance of the class it's serialized from, there's got to be some way to make an association. That's what the '__namespace' in the functions above is for, but that may not be enough by itself. The other piece of the puzzle is the idea of registering each JSON-loadable class, and keeping track of those classes so that they can be quickly identified, and their FromJSON methods can be called. Since I haven't detailed IsJSONSerializable yet, there's no context for how that works (it turned into a circular reference), but it does work, at least with the limited testing I've done so far.

def wrapjsonload( origfunc ):
    """
Replaces json.load with a function that hands processing off to a subclass of 
IsJSONSerializable for unserialization of the JSON data into an instance of the 
class when applicable"""
    if origfunc != json.load:
        raise RuntimeError( 'wrapjsonload expects json.load as the '
            'function to decorate, but was passed %s' % ( origfunc ) )
    if IsJSONSerializable._decoratedJSON.get( origfunc ):
        return IsJSONSerializable._decoratedJSON[ origfunc ]
    def _load( *args, **kw ):
        baseDict = origfunc( *args, **kw )
        try:
            objNS = baseDict.get( '__namespace' )
        except AttributeError:
            objNS = None
        if objNS:
            if type( baseDict ) != dict:
                raise ValueError( 'Decorated override of json.loads '
                    'expected a dict value to convert to an instance of '
                    'IsJSONSerializable, but the supplied JSON evaluated '
                    'to "%s" (%s)' % ( 
                        baseDict, type( baseDict ).__name__
                    )
                )
            objClass = IsJSONSerializable._registeredLoadables.get( objNS )
            if objClass:
                return objClass.FromDict( baseDict )
            raise RuntimeError( 'decorated override of json.load could not '
                'find a valid object-namespace (%s) to work with: %s' % ( 
                objNS, args[ 0 ] ) )
        return baseDict
    IsJSONSerializable._decoratedJSON[ origfunc ] = _load
    return _load

Here's a walkthrough of what happens when the replacement function for json.load is called:

  • A call to json.load is made, with JSON to be unserialized.
    • Since json.load has been replaced with _load by the decorator, _load is called instead:
      • The original function (preserved, again, within the scope of the replacement function) is called to get a dict.
      • That dict is checked for a '__namespace' key:
        • If the namespace exists, then the dictionary of registered loadable classes (IsJSONSerializable._registeredLoadables is checked for a match
        • If there is a match, then the found class' FromDict class-method is called, and the results returned
      • If the namespace doesn't have a registered class, then the dict that was initially retrieved is returned instead

This process has added a few things to the IsJSONSerializable interface and implementation requirements:

  • A method for registering IsJSONSerializable classes;
  • Some class-level attributes in IsJSONSerializable for keeping track of registered IsJSONSerializable classes, keyed on their Python namespace;
  • A way to find that Python namespace;

The wrapjsonloads function is, apart from the signature of the replacement function itself and what original function it's expecting, identical to wrapjsonload. There's not much about it to comment on, but I'm going to show it in the interests of being thorough:

def wrapjsonloads( origfunc ):
    """
Replaces json.loads with a function that hands processing off to a subclass of 
IsJSONSerializable for unserialization of the JSON data into an instance of the 
class when applicable"""
    if origfunc != json.loads:
        raise RuntimeError( 'wrapjsonloads expects json.loads as the '
            'function to decorate,but was passed %s' % ( origfunc ) )
    if IsJSONSerializable._decoratedJSON.get( origfunc ):
        return IsJSONSerializable._decoratedJSON[ origfunc ]
    def _loads( *args, **kw ):
        baseDict = origfunc( *args, **kw )
        try:
            objNS = baseDict.get( '__namespace' )
        except AttributeError:
            objNS = None
        if objNS:
            if type( baseDict ) != dict:
                raise ValueError( 'Decorated override of json.loads '
                    'expected a dict value to convert to an instance of '
                    'IsJSONSerializable, but the supplied JSON evaluated '
                    'to "%s" (%s)' % ( 
                        baseDict, type( baseDict ).__name__
                    )
                )
            objClass = IsJSONSerializable._registeredLoadables.get( objNS )
            if objClass:
                return objClass.FromDict( baseDict )
            raise RuntimeError( 'decorated override of json.loads could '
                'not find a valid object-namespace (%s) to work with: %s' % 
                ( objNS, args[ 0 ] )
            )
        return baseDict
    IsJSONSerializable._decoratedJSON[ origfunc ] = _loads
    return _loads

This is a bit longer than I'd like already, and this feels like a reasonable break-point, so I'll pick up again in my next post with the IsJSONSerializable abstract class, a fairly detailed look at what needs to be done to build a derived class. I'm also planning on showing an example structure that I'll be able to show in action (if only through the command-line), but I think that'll be long enough to warrant its own post.