moo otypes is way to produce instances of oschema types that tries to follow a valid-by-construction pattern. In general, otypes are native language types (eg those of Python) which derive from oschema schema data structures.
For example, in the oschema document we saw some basic use of the
general otypes pattern applied as C++ codegen in the structs.hpp.j2
and nljs.hpp.j2
templates. When instantiating a codegen’ed C++ struct
we have some immediate validity guarantees. They may then
automatically transfer to a JSON object that may be derived from the
C++ struct
.
The rest of this document describes how moo applies the otypes pattern
in Python. With the use of the moo.otypes
Python module, user code
may access Python classes corresponding to oschema types and in
instantiating them as Python objects gain some valid-by-construction
guarantees.
The moo.otypes
module provides high level functions to construct types
from oschema. They make use of lower-level Python metaprogramming for
the actual type construction which is also in this module.
We may make an individual type from oschema data structure which is
expressed as keyword arguments to the moo.otypes.make_type()
function.
For example:
import moo.otypes
Age = moo.otypes.make_type(
name="Age", doc="An age in years",
schema="number", dtype='i4', path='a.b')
myage = Age(42) # my forever age
print(f'{Age}, {myage}, {myage.pod()}')
import a.b
myage2 = Age(18)
print(f'{a.b.Age}, {myage2}, {myage2.pod()}')
<class 'a.b.Age'>, <number Age: 42>, 42 <class 'a.b.Age'>, <number Age: 18>, 18
We may try to use our otype with invalid data:
myage = Age("older than the hills")
Which will return an error like:
Traceback (most recent call last): File "<stdin>", line 8, in <module> File "<number Age>", line 13, in __init__ File "/home/bv/dev/moo/moo/otypes.py", line 485, in update self._value = numpy.array(val, dtype) ValueError: invalid literal for int() with base 10: 'older than the hills'
This error illustrates how the valid-by-construction pattern works. It’s not that magic is performed and the data is miraculously valid. Rather, we the developer are immediately punished if we attempt to violate the schema.
Creation of a system of related types is done with an array of data
structures describing its schema and the Python types are constructed
with the plural function moo.otypes.make_types()
. For example:
import moo.otypes
schema = [dict(name="Pet", schema="enum",
symbols=["cat", "dog", "early personal computer"],
default="cat",
path="my.home.office",
doc="A kind of pet"),
dict(name="Desk", schema="record",
fields=[
dict(name="ontop", item="my.home.office.Pet"),
],
path="my.home.office",
doc="Model my desk")]
moo.otypes.make_types(schema)
from my.home.office import Desk, Pet
desk = Desk(ontop="cat")
print(f'{Desk}, {desk}, {desk.pod()}, {desk.ontop}')
<class 'my.home.office.Desk'>, <record Desk, fields: {ontop}>, {'ontop': 'cat'}, cat
The highest layer function is moo.otypes.load_types()
. It is a mere
convenience function that combines the moo method to load a schema
file and a call to make_types()
on the result.
We can see load_types()
in action using a schema that is part of the
moo test suite:
import os
import moo.otypes
# Directly make a type in Python to use for an "any" below
moo.otypes.make_type(schema="string", name="Uni", path="test.basetypes")
from test.basetypes import Uni
# We locate and load a test schema file
here = os.path.join(os.path.dirname(__file__), "test")
types = moo.otypes.load_types("test-ogen-oschema.jsonnet", [here])
from app import Person
per = Person(email="[email protected]", counts=[42],
affil=Uni("Snooty U"), mbti="judging")
print(f'{Person}, {per}, {per.affil}')
print(per.pod())
<class 'app.Person'>, <record Person, fields: {email, email2, counts, affil, mbti}>, Snooty U {'email': '[email protected]', 'email2': '[email protected]', 'counts': [42], 'affil': 'Snooty U', 'mbti': 'judging'}
Type information and any doc
strings given in the schema are reflected
into the constructed Python types. This information can be displayed
using usual Python meta interrogation methods. For example:
import os
import moo.otypes
import moo.io
here = os.path.join(os.path.dirname(__file__), "test")
moo.io.default_load_path = [here]
types = moo.otypes.load_types("test-ogen-oschema.jsonnet")
from app import Person
help(Person)
Help on class Person in module app: class Person(moo.otypes._Record) | Person(*args, email: app.Email = None, email2: app.Email = '[email protected]', counts: app.Counts = None, affil: app.Affiliation = None, mbti: app.MBTI = 'introversion') | | Record type Person with fields: "email", "email2", "counts", "affil", "mbti" | | Describe everything there is to know about an individual human | | Method resolution order: | Person | moo.otypes._Record | moo.otypes.BaseType | abc.ABC | builtins.object | | Methods defined here: | | __init__(self, *args, email: app.Email = None, email2: app.Email = '[email protected]', counts: app.Counts = None, affil: app.Affiliation = None, mbti: app.MBTI = 'introversion') | Create a record type of Person | | ---------------------------------------------------------------------- | Data descriptors defined here: | | affil | | counts | | email | | email2 | | mbti | | ---------------------------------------------------------------------- | Data and other attributes defined here: | | __abstractmethods__ = frozenset() | | ---------------------------------------------------------------------- | Methods inherited from moo.otypes._Record: | | __repr__(self) | Return repr(self). | | pod(self) | Return record as plain old data. | | Will perform validation. | | update(self, *args, **kwds) | Update a record. | | An arg in args may be one of: | - a JSON string | - a dictionary | - an instance of a record of same type. | | kwds may be a dictionary. | | Dictionaries are taken to be field settings, values can be POD | or a typed object consistent with the field type. | | ---------------------------------------------------------------------- | Readonly properties inherited from moo.otypes._Record: | | field_names | Return list of field names | | fields | Return mapping of field name to field dict | | ---------------------------------------------------------------------- | Readonly properties inherited from moo.otypes.BaseType: | | ost | The object schema type | | ---------------------------------------------------------------------- | Data descriptors inherited from moo.otypes.BaseType: | | __dict__ | dictionary for instance variables (if defined) | | __weakref__ | list of weak references to the object (if defined)