Validate configuration and produce human readable error messages.


License
MIT
Install
pip install cfgv==3.4.0

Documentation

build status pre-commit.ci status

cfgv

Validate configuration and produce human readable error messages.

Installation

pip install cfgv

Sample error messages

These are easier to see by example. Here's an example where I typo'd true in a pre-commit configuration.

pre_commit.clientlib.InvalidConfigError:
==> File /home/asottile/workspace/pre-commit/.pre-commit-config.yaml
==> At Config()
==> At key: repos
==> At Repository(repo='https://github.com/pre-commit/pre-commit-hooks')
==> At key: hooks
==> At Hook(id='flake8')
==> At key: always_run
=====> Expected bool got str

API

cfgv.validate(value, schema)

Perform validation on the schema:

  • raises ValidationError on failure
  • returns the value on success (for convenience)

cfgv.apply_defaults(value, schema)

Returns a new value which sets all missing optional values to their defaults.

cfgv.remove_defaults(value, schema)

Returns a new value which removes all optional values that are set to their defaults.

cfgv.load_from_filename(filename, schema, load_strategy, exc_tp=ValidationError)

Load a file given the load_strategy. Reraise any errors as exc_tp. All defaults will be populated in the resulting value.

Most useful when used with functools.partial as follows:

load_my_cfg = functools.partial(
    cfgv.load_from_filename,
    schema=MY_SCHEMA,
    load_strategy=json.loads,
    exc_tp=MyError,
)

Making a schema

A schema validates a container -- cfgv provides Map and Array for most normal cases.

writing your own schema container

If the built-in containers below don't quite satisfy your usecase, you can always write your own. Containers use the following interface:

class Container(object):
    def check(self, v):
        """check the passed in value (do not modify `v`)"""

    def apply_defaults(self, v):
        """return a new value with defaults applied (do not modify `v`)"""

    def remove_defaults(self, v):
        """return a new value with defaults removed (do not modify `v`)"""

Map(object_name, id_key, *items)

The most basic building block for creating a schema is a Map

  • object_name: will be displayed in error messages
  • id_key: will be used to identify the object in error messages. Set to None if there is no identifying key for the object.
  • items: validator objects such as Required or Optional

Consider the following schema:

Map(
    'Repo', 'url',
    Required('url', check_any),
)

In an error message, the map may be displayed as:

  • Repo(url='https://github.com/pre-commit/pre-commit')
  • Repo(url=MISSING) (if the key is not present)

Array(of, allow_empty=True)

Used to nest maps inside of arrays. For arrays of scalars, see check_array.

  • of: A Map / Array or other sub-schema.
  • allow_empty: when False, Array will ensure at least one element.

When validated, this will check that each element adheres to the sub-schema.

Validator objects

Validator objects are used to validate key-value-pairs of a Map.

writing your own validator

If the built-in validators below don't quite satisfy your usecase, you can always write your own. Validators use the following interface:

class Validator(object):
    def check(self, dct):
        """check that your specific key has the appropriate value in `dct`"""

    def apply_default(self, dct):
        """modify `dct` and set the default value if it is missing"""

    def remove_default(self, dct):
        """modify `dct` and remove the default value if it is present"""

It may make sense to borrow functions from the built in validators. They additionally use the following interface(s):

  • self.key: the key to check
  • self.check_fn: the check function
  • self.default: a default value to set.

Required(key, check_fn)

Ensure that a key is present in a Map and adheres to the check function.

RequiredRecurse(key, schema)

Similar to Required, but uses a schema.

Optional(key, check_fn, default)

If a key is present, check that it adheres to the check function.

  • apply_defaults will set the default if it is not present.
  • remove_defaults will remove the value if it is equal to default.

OptionalRecurse(key, schema, default)

Similar to Optional but uses a schema.

  • apply_defaults will set the default if it is not present and then validate it with the schema.
  • remove_defaults will remove defaults using the schema, and then remove the value it if it is equal to default.

OptionalNoDefault(key, check_fn)

Like Optional, but does not apply_defaults or remove_defaults.

Conditional(key, check_fn, condition_key, condition_value, ensure_absent=False)

  • If condition_key is equal to the condition_value, the specific key will be checked using the check function.
  • If ensure_absent is True and the condition check fails, the key will be checked for absense.

Note that the condition_value is checked for equality, so any object implementing __eq__ may be used. A few are provided out of the box for this purpose, see equality helpers.

ConditionalOptional(key, check_fn, default, condition_key, condition_value, ensure_absent=False)

Similar to Conditional and Optional.

ConditionalRecurse(key, schema, condition_key, condition_value, ensure_absent=True)

Similar to Conditional, but uses a schema.

NoAdditionalKeys(keys)

Use in a mapping to ensure that only the keys specified are present.

Equality helpers

Equality helpers at the very least implement __eq__ for their behaviour.

They may also implement def describe_opposite(self): for use in the ensure_absent=True error message (otherwise, the __repr__ will be used).

Not(val)

Returns True if the value is not equal to val.

In(*values)

Returns True if the value is contained in values.

NotIn(*values)

Returns True if the value is not contained in values.

Check functions

A number of check functions are provided out of the box.

A check function takes a single parameter, the value, and either raises a ValidationError or returns nothing.

check_any(_)

A noop check function.

check_type(tp, typename=None)

Returns a check function to check for a specific type. Setting typename will replace the type's name in the error message.

For example:

Required('key', check_type(int))
# 'Expected bytes' in both python2 and python3.
Required('key', check_type(bytes, typename='bytes'))

Several type checking functions are provided out of the box:

  • check_bool
  • check_bytes
  • check_int
  • check_string
  • check_text

check_one_of(possible)

Returns a function that checks that the value is contained in possible.

For example:

Required('language', check_one_of(('javascript', 'python', 'ruby')))

check_regex(v)

Ensures that v is a valid python regular expression.

check_array(inner_check)

Returns a function that checks that a value is a sequence and that each value in that sequence adheres to the inner_check.

For example:

Required('args', check_array(check_string))

check_and(*fns)

Returns a function that performs multiple checks on a value.

For example:

Required('language', check_and(check_string, my_check_language))