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