How to 'dazlize' nested Python class instances

TLDR

A few lines of code can help using effectively Python custom classes representing complex Daml data types.

Why Python and Dazl?

You may have different reasons for using Dazl rather than Java or Javascript which utilize the convenience of codegen.

My motivation is that I am a big fan of Google Colab notebooks for what is sometimes called “exploratory programing”, that is prototyping, demoing, or just simply printing out the ledger content, the return value of choices, or the ledger history processing the transaction tree.

Colab notebooks are optimized for Python, and it’s a lucky coincidence that we can easily use the Dazl library with the notebooks.

The challenge

The flip side of the convenience of Python and Colab is the lack of type checking and Daml codegen.

It seems to be straightforward to make up for these handicaps with hand-written Python classes. The problem is that the Dazl API expects Python dictionaries with the same field names and value types as the corresponding Daml types, rather than custom class instances.

The simple case

With simple classes this is not a big issue, because the vars Python function applied to simple class instances returns exactly what we need.

An example is the Give choice input type for the Asset contract in the skeleton template:

template Asset
  with
    issuer : Party
    owner  : Party
    name   : Text
  where
    ensure name /= ""
    signatory issuer
    observer owner
    choice Give : AssetId
      with
        newOwner : Party
      controller owner
      do create this with
           owner = newOwner

The Daml record data type for the choice input is the following:

data Give = Give with
    newOwner : Party
  deriving (Eq, Show)

The corresponding Python class is the following:

class Give:
  def __init__(self, newOwner):
    if type(newOwner) is not str:
      raise TypeError(f'{newOwner} must be a string')
    else:
      self.newOwner = newOwner

It can be used in the following way:

give = Give('alice')
vars(give)

The result will be: {'newOwner': 'alice'}

And it can be used to submit a command with Dazl like this:

async with dazl.connect(**ENV) as conn:
    await conn.exercise(<contract id>, 'Give', vars(give))

The only problem is that if some of the values of the class instance are themselves class instances, this doesn’t work. The vars function only works in the outermost layer. Let’s see an example for this:

class Coordinates:
  def __init__(self, x, y):
    if type(x) is not float:
      raise TypeError(f'{x} must be a float')
    if type(y) is not float:
      raise TypeError(f'{y} must be a float')
    else:
      self.x = x
      self.y = y

class Rectangle:
  def __init__(self, top_left_corner, bottom_right_corner):
    if type(top_left_corner) is not Coordinates:
      raise TypeError(f'The type of {top_left_corner} must be Coordinates')
    if type(bottom_right_corner) is not Coordinates:
      raise TypeError(f'The type of {bottom_right_corner} must be Coordinates')
    else:
      self.top_left_corner = top_left_corner
      self.bottom_right_corner = bottom_right_corner

rectangle = Rectangle(Coordinates(0.0, 0.0), Coordinates(3.0, 4.0))

vars(rectangle)

The result will be, which is not accepted by Dazl:

{'top_left_corner': <__main__.Coordinates at 0x7f583d875fd0>,
 'bottom_right_corner': <__main__.Coordinates at 0x7f583d8753d0>}

The solution for the complex case

For nested custom class instances we need something slightly more complex than the vars function, namely a function which recursively converts all levels of the object to the corresponding dict, so that if it’s already a dict, it does nothing. I’ve also included the cases when we only use a Python class for checking a condition and the value is stored in the ‘value’ field. In the latter case we need the value of the ‘value’ field returned.

Such a function can is the following:

def dazlize(obj):
  if type(obj) is not dict and not getattr(obj, '__dict__', None):
    return obj
  if getattr(obj, '__dict__', None) and list(obj.__dict__.keys()) == ['value']:
    return obj.__dict__['value']
  else:
    if type(obj) is dict:
      items = obj.items()
    if getattr(obj, '__dict__', None):
      items = obj.__dict__.items()
    return {key: dazlize(value) for key,value in items}

If we apply this function to the rectangle object above, we get a nested dict which is accepted by Dazl (provided if the Daml model contains the correspoinding Daml values):

{'top_left_corner': {'x': 0.0, 'y': 0.0}, 'bottom_right_corner': {'x': 3.0, 'y': 4.0}}