User Documentation¶
User objects API¶
To build custom user object definitions in a declarative style,
you do so by creating subclasses of wysdom.UserObject.
Instances of your subclass will behave as a MutableMapping,
so any code that works on the underlying dict that you use to
populate it should also work on the object instance.
There are two ways to add properties to a UserObject. The first
is to add named properties, by using the wysdom.UserProperty
data descriptor:
class Person(UserObject):
first_name = UserProperty(str)
The second is to allow dynamically named additional properties:
class Person(UserObject, additional_properties=True):
...
Any additional properties that are not explicitly defined as named attributes using the UserProperty descriptor must be accessed using the subscript style, object_instance[property_name].
You may also restrict the data types of the additional properties
that you will allow. The type parameter that you pass in to
additional_properties can be a primitive Python type, a subclass
of UserProperty, or an instance of Schema:
class Person(UserObject, additional_properties=str):
...
class Person(UserObject, additional_properties=Address):
...
class Person(UserObject, additional_properties=SchemaDict(Vehicle)):
...
Property Types¶
The type parameter that you pass in to UserProperty can be a primitive
Python type, a subclass of UserProperty, or an instance of Schema:
class Person(UserObject):
first_name = UserProperty(str)
last_name = UserProperty(str)
current_address = UserProperty(Address)
previous_addresses = UserProperty(SchemaArray(Address))
vehicles = UserProperty(SchemaDict(Vehicle))
Property Naming¶
If a UserProperty is not explicitly given a name, it is populated using the attribute name that it is given on the parent class. If you want the name of the attribute in the class to be different from the key in the underlying data that is supplied to the object, you may specify it explicitly using the name parameter:
class Person(UserObject):
last_name = UserProperty(str, name="surname")
Optional Properties¶
If a UserProperty is intended to be an optional property in the underlying data object, it can be specified as such using the optional parameter:
class Person(UserObject):
...
middle_name = UserProperty(str, optional=True)
If a parameter is not specified as optional, it will appear in the required list in the generated JSON schema and will therefore throw a ValidationError if it is missing from any underlying data object that is loaded.
If optional properties do not have a default or default_function, they will default to None if not set.
Defaults¶
If you need a UserProperty to have a default value, you may give it a static value using the default parameter:
class Person(UserObject):
first_name = UserProperty(str, default="")
Or if you need the default value to have a dynamic value based on other properties, you may use the default_function parameter:
class Person(UserObject):
...
known_as = UserProperty(
str,
default_function=lambda person: person.first_name
)
A UserProperty may not have both a default and a default_function, and if either default or default_function is set then optional defaults to True (and cannot be explicitly set to False).
Constants¶
Sometimes a property should always have one constant value for a given schema. A common use case is for properties that identify an object as a particular object type.
In this case, use the wysdom.SchemaConst class:
pet_type = UserProperty(SchemaConst("cat"))
Enums¶
Sometimes a property should have one of a set of strictly enumerated values.
For this use case it is possible to use a Python Enum as the property type
of a given UserProperty:
from enum import Enum
class Color(Enum):
PINK = "pink"
ORANGE = "orange"
class Vehicle(UserObject):
color = UserProperty(Color)
>>> my_vehicle = Vehicle({"color": "orange"})
>>> my_vehicle.color
<Color.ORANGE: 'orange'>
When this object is translated to a JSON Schema, the enum keyword will be used to define the permitted values of the property.
Patterns¶
If you need to restrict the values that a UserProperty can take according to a regex pattern, you can specify this using the pattern parameter:
class Vehicle(UserObject):
rgb_hex_color = UserProperty(str, pattern=r"^[0-9a-f]{6}$")
Note that this will throw a TypeError if any type other than str is supplied.
When the object is translated to a JSON Schema, the pattern keyword will be used to validate the permitted values of the property.
Arrays and Dicts¶
For complex schemas, it is often necessary to declare a property as being an array or a dictionary or other objects.
For an array, use the wysdom.SchemaArray. Properties of this type
function identically to a Python list (specifically a
collections.abc.MutableSequence):
related_people = UserProperty(SchemaArray(Person))
From 0.3.0, you can use the data descriptor wysdom.ListProperty
to aid readability by equivalently writing:
related_people = ListProperty(Person)
For an dictionary, use the wysdom.SchemaDict. Properties of this type
function identically to a Python dict (specifically a
collections.abc.MutableMapping with keys of type str):
related_people = UserProperty(SchemaDict(Person))
From 0.3.0, you can use the data descriptor wysdom.DictProperty
to aid readability by equivalently writing:
related_people = DictProperty(Person)
A SchemaDict is a special case of a wysdom.SchemaObject with
no named properties and with additional_properties set to the type
specification that you supply.
For both SchemaArray and SchemaDict you may pass in any type definition that you would pass to a UserProperty.
Dict Key Validation via Regex¶
If your dictionary only has certain keys that are valid for your application according to a regex pattern, you can specify this with the parameter key_pattern:
color_names = UserProperty(
SchemaDict(ColorName),
key_pattern=r"^[0-9a-f]{6}$"
)
This will translate to the following in the object’s JSON Schema definition:
"propertyNames": {
"pattern": "^[0-9a-f]{6}$"
}
DOM functions¶
While the DOM and schema information can be retrieved from a DOMElement using the __json_dom_info__ property and __json_schema__() method respectively, the following convenience functions are provided for code readability.
-
wysdom.document(element: wysdom.dom.DOMElement.DOMElement) → Optional[wysdom.dom.DOMElement.DOMElement]¶ Retrieve the owning document for a
DOMElement, if it exists.- Parameters
element – A DOM element
- Returns
The owning document for that DOM element, or None if none exists
-
wysdom.parent(element: wysdom.dom.DOMElement.DOMElement) → Optional[wysdom.dom.DOMElement.DOMElement]¶ Retrieve the parent element of a
DOMElement, if it exists.- Parameters
element – A DOM element
- Returns
The parent element of that DOM element, or None of none exists
-
wysdom.key(element: wysdom.dom.DOMElement.DOMElement) → Optional[str]¶ Retrieve the key of a particular
DOMElementin its parent element, if it can be referred to by a key (i.e. if it its parent element is acollections.abc.Mapping).- Parameters
element – A DOM element
- Returns
The key of that DOM element in its parent, or None if it has no key
-
wysdom.schema(element: wysdom.dom.DOMElement.DOMElement) → wysdom.base_schema.Schema.Schema¶ Retrieve the
Schemaobject for a particularDOMElement.- Parameters
element – A DOM element
- Returns
The
Schemaobject associated with that DOM element
Mixins¶
The interface for UserObject has been kept as minimal as possible to avoid cluttering the interfaces of user subclasses with unnecessary methods. However, there is some common functionality, such as reading and writing JSON and YAML
ReadsJSON¶
Usage: As in the first usage example, but add wysdom.mixins.ReadsJSON
to the bases of Person:
class Person(UserObject, ReadsJSON):
first_name = UserProperty(str)
last_name = UserProperty(str)
current_address = UserProperty(Address)
previous_addresses = UserProperty(SchemaArray(Address))
person_instance = Person.from_json(
"""
{
"first_name": "Marge",
"last_name": "Simpson",
"current_address": {
"first_line": "123 Fake Street",
"second_line": "",
"city": "Springfield",
"postal_code": 58008
},
"previous_addresses": [{
"first_line": "742 Evergreen Terrace",
"second_line": "",
"city": "Springfield",
"postal_code": 58008
}],
"vehicles": {
"eabf04": {
"color": "orange",
"description": "Station Wagon"
}
}
}
"""
)
ReadsYAML¶
Usage: As in the first usage example, but add wysdom.mixins.ReadsYAML
to the bases of Person:
class Person(UserObject, ReadsYAML):
first_name = UserProperty(str)
last_name = UserProperty(str)
current_address = UserProperty(Address)
previous_addresses = UserProperty(SchemaArray(Address))
person_instance = Person.from_yaml(
"""
first_name: Marge
last_name: Simpson
current_address:
first_line: 123 Fake Street
second_line: ''
city: Springfield
postal_code: 58008
previous_addresses:
- first_line: 742 Evergreen Terrace
second_line: ''
city: Springfield
postal_code: 58008
vehicles:
eabf04:
color: orange
description: Station Wagon
"""
)
RegistersSubclasses¶
Use wysdom.mixins.RegistersSubclasses as a mixin if you want an abstract base class to
have several more specific subclasses:
class Pet(UserObject, RegistersSubclasses, ABC):
pet_type: str = UserProperty(str)
name: str = UserProperty(str)
@abstractmethod
def speak(self):
pass
class Dog(Pet):
pet_type: str = UserProperty(SchemaConst("dog"))
def speak(self):
return f"{self.name} says Woof!"
class Cat(Pet):
pet_type: str = UserProperty(SchemaConst("cat"))
def speak(self):
return f"{self.name} says Miaow!"
If you use RegistersSubclasses, you may refer to the abstract base class when defining properties and schemas in wysdom. When the DOM is populated with data, the subclass which matches the supplied data’s schema will automatically be chosen:
class Person(UserObject):
pets = UserProperty(SchemaArray(Pet))
person_instance = Person({
"pets": [{
"pet_type": "dog",
"name": "Santa's Little Helper"
}]
})
>>> type(person_instance.pets[0])
<class '__main__.Dog'>
If you include an abstract base class in an object definition, it will be represented in the JSON schema using the SchemaAnyOf with all of the defined subclasses as allowed options.
Registering classes by name¶
If your application needs to look up registered subclasses by a key, you may supply the register_as keyword when declaring a subclass:
class Elephant(Pet, register_as="elephant"):
pet_type: str = UserProperty(SchemaConst("elephant"))
def speak(self):
return f"{self.name} says Trumpet!"
You may then use the class’s registered name to look up the class or create an instance from its parent class:
>>> Pet.registered_subclass("elephant")
<class '__main__.Elephant'>
>>> Pet.registered_subclass_instance("elephant",
... {"pet_type": "elephant", "name": "Stampy"}).speak()
'Stampy says Trumpet!'
Internals¶
Schemas¶
Base schemas¶
The following schemas define simple atomic schemas (defined in the subpackage wysdom.base_schema):
Name |
Description |
|---|---|
abstract base class |
|
abstract base class for any schema with the “type” directive |
|
any valid JSON will be accepted |
|
a string constant |
|
a null value |
|
a primitive variable |
Object schemas¶
The following schemas define complex schemas which reference other schemas (defined in the subpackage wysdom.object_schema):
Name |
Description |
|---|---|
Any of the permitted schemas supplied |
|
An array (corresponding to a Python list) |
|
An object with named properties |
|
An object with dynamic properties (corresponding to a Python dict) |