Recipes¶
Validate Configuration On Load¶
The Schema
object has a validator()
decorator that
registers a method as a validator. Whenever the configuration is loaded, the schema’s validators
are run against the configuration. Custom config validators can perform advanced validations, like
the following:
schema = Schema()
schema.x = IntField()
schema.y = IntField()
schema.db.username = StringField()
schema.db.password = StringField()
@validator(schema)
def validate_x_lt_y(cfg):
# validates that x < y
if cfg.x and cfg.y and cfg.x >= cfg.y:
raise ValueError('x must be less-than y')
@validator(schema.db)
def validate_db_credentials(cfg):
# validates that if the db username is specified then the password must
# also be specified.
if cfg.username and not db.password:
raise ValueError('db.password is required when username is specified')
config = schema()
config.load('config.json', format='json') # will call the above validators
Allow Multiple Configuration Files¶
The IncludeField
allows users to specify an additional file to parse when
loading the config from disk. The IncludeField
is processes prior to setting parsing any
of the configuration values. So, the entire config file and any included config files are combined
in memory prior to parsing.
schema = Schema()
schema.include = IncludeField()
# other fields
config = schema()
config.load('config.json', format='json')
config.json
{
"include": "other_file.json"
}
IncludeFields can occur anywhere in a configuration, however, when the included file is processed, it is handled in the same scope as where the IncludeField was defined.
Dynamically Get and Set Configuration Values¶
The Config
class supports getting and setting config values via both direct
attribute access and the __getitem__
and __setitem__
protocols. The __getitem__
and
__setitem__
methods have the added benefit of supporting getting and setting nested config
values.
schema = Schema()
schema.x = IntField()
schema.db.port = IntField(default=27017)
schema.db.host = HostnameField(default='127.0.0.1', allow_ipv4=True)
config = schema()
config.load('config.json', format='json')
#
# get the set port
# equivalent to:
#
# print(config.db.port)
#
print(config['db.port'])
#
# set the hostname
# equivalent to:
#
# config.db.host = 'db.example.com'
#
config['db.host'] = 'db.example.com'
Using __getitem__
and __setitem__
is useful in situations where you need dynamic
programmatic access to the configuration values, such as supporting a generic REST API to interact
with the configuration.
List Field of Complex Types¶
The ListField
performs value validation for each item by accepting either a
Field
or Schema
. Consider the example of a list of
webhooks.
webhook_schema = Schema()
webhook_schema.url = UrlField(required=True)
webhook_schema.verify_ssl = BoolField(default=True)
schema = Schema()
schema.issue_webhooks = ListField(webhook_schema)
schema.merge_request_webhooks = ListField(webhook_schema)
config = schema()
wh = webhook_schema()
wh.url = 'https://google.com'
config.issue_webhooks.append(wh)
Here, the webhook
schema is usable across multiple configurations. As seen here, it is not
very intuitive to create reusable configuration items. The make_type()
method
is designed to make working with these reusable configurations easier. make_type
creates a new
type, inheriting from Config
that is more Pythonic.
webhook_schema = Schema()
webhook_schema.url = UrlField(required=True)
webhook_schema.verify_ssl = BoolField(default=True)
WebHook = make_type(webhook_schema, 'WebHook') # WebHook is now a new type
schema = Schema()
schema.issue_webhooks = ListField(webhook_schema)
schema.merge_request_webhooks = ListField(webhook_schema)
config = schema()
wh = WebHook(url='https://google.com')
config.issue_webhooks.append(wh)