Acspec allows you to specify your schematics models with python dicts or YAML files.
from acspec.base import Acspec
models = Acspec({
"todo_list": {
"todos": {
"type": "list",
"list": {
"type": "string"
}
}
}
})
todo_list = models.TodoListModel({"todos": ["write test", "write library"]})
todo_list.validate()
There are simple types, and 3 compound types:
list, dict and model.
Note, the type, e.g. "type": "list"
, is optional for compound types but may
be added for consistency.
"base": BaseType
"boolean": BooleanType
"date_time": DateTimeType
"date": DateType
"email": EmailType
"float": FloatType
"integer": IntType
"long": LongType
"string": StringType
"timestamp": DateType
"url": URLType
"model": ModelType
"list": ListType
"dict": DictType
The model name is given as lowercase, underscored string and will be converted to the camelized version plus the "Model"-suffix. E.g. blog_post => BlogPostModel
You can override the suffix with the model_suffix option.
Model names should be valid python identifiers. If not, they will be sanitized
by default. You can customize this behavior by setting the
on_invalid_identifier
option to skip
or raise
.
You can use the schematics options and validations in your specs:
acspec = Acspec({"todo": {
"title": {
"type": "string",
"max_length": 20,
"min_length": 3,
}
}})
Meta information can be specified with the ":"-prefix, e.g. override the model's name like this:
acspec = Acspec({"todo": {
":name": "MyTodoModel"
"title": {
"type": "string"
}
}})
acspec.MyTodoModel
The models can inherit from each other
acspec = Acspec(
{
"base_message": {
"text": {
"type": "string"
}
},
"message": {
":bases": ["base_message"],
"title": {
"type": "string"
}
},
})
assert issubclass(acspec.MessageModel, acspec.BaseMessageModel)
The models can inherit from models defined somewhere else. To enable acspec to use and resolve those models, you need to provide a class_mapping:
class_mapping = {
"base": CustomModel
}
acspec = Acspec(
{
"test": {
":bases": ["base"]
# your attributes
}
}, class_mapping=class_mapping)
assert issubclass(acspec.TestModel, CustomModel)
As it's a very common case, Acspec ships with the DontSerializeWhenNoneModel, so you can always:
acspec = Acspec(
{
"test": {
":bases": ["dont_serialize_when_none"]
}
})
The model type enables you to reference/nest other models.
models = Acspec({
"todo_list": {
"todos": {
"type": "list",
"list": {
"type": "model",
"model": "todo"
}
}
},
"todo": {
"title": {
"type": "string"
}
}
})
my_todo_list = models.TodoListModel({
"todos": [
{"title": "write tests"},
{"title": "write docs"}
]
})
For prototyping you can use the experimental feature to load the models in a python module. This way you can import them like any class defined in python.
from acspec.base import Acspec
models = Acspec({
"todo": {
"title":{
"type": "string"
}
}
})
# default module name is acspecctx
models.create_or_update_sys_module("todos")
from todos import TodoModel
# and use your model
Instead of passing a dict to define your spec models, you can also use YAML from your file system with the Yspec class.
- load a file: create a model from every key on root.
- load a directory: create a model for every file
from acspec.yspec import Yspec
acspec = Yspec.load("path/to/your/yaml/files")
# use your models
For more examples see test_acspec/test_yspec.py
Your models may need custom and helper methods. If inheritance (see above) is not flexible enough for you, consider assigning the methods afterwards
def get_identifier(self):
return self.id
acspec.TestModel.get_identifier = get_identifier
or extending the model
class TestModel(acspec.TestModel):
def get_identifier(self):
return self.id
You may have custom options and handling for your specs and require an extended input format. The simplest way to achieve this is to provide a type descriptor mixin with your additional fields or overrides:
class MyTypeDescriptorMixin(Model):
other_field = StringType()
@property
def non_kwarg_keys(self):
return super().non_kwarg_keys | {"other_field"}
def init_schematics_type(self, context=None):
type_class = super(MyTypeDescriptorMixin, self).init_schematics_type(
context=context
)
# extend or customize the schematics field descriptor
# ...
return type_class
# Make it strict: raise when encounter an unknown key / rogue field
MyDescriptionClass = build_description_class(
type_descriptor_mixin=MyTypeDescriptorMixin, strict=True
)
models = Acspec(
{
"todo": {
"title":{
"type": "string",
"other_field": "other_value"
}
}
},
SchematicsModelBuilder(
description_class=MyDescriptionClass
)
)
Note: Composing the description class is a complex operation. Bear in mind to initialize your custom description only once.
Provide a Python module (must be importable) as parameter and the script will try to transform the contained Schematics models to acspec YAML files.
Usage:
env/bin/extract_specs some.module.name
# will output the specs as YAML. Use -d option for output directory
Example:
env/bin/extract_specs test_acspec.extract.conftest -d tmp/models
All rules that need virtualenv accept a PYTHON variable. If set to 'python3',
virtualenv -p python3
will be used, else the default.
To set up the environment for python2 and 3:
make all-platforms
To run tests with default virtualenv (python2):
make test
Python3:
make test PYTHON=python3
All:
make test-all-platforms
The test rules accept a TEST variable to make it easy to test single files:
make test-all-platforms TEST=test_acspec/test_model.py