diff options
Diffstat (limited to 'README.md')
-rw-r--r-- | README.md | 282 |
1 files changed, 282 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b8f227 --- /dev/null +++ b/README.md @@ -0,0 +1,282 @@ +[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/asottile.cfgv?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=24&branchName=master) +[![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/24/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=24&branchName=master) + +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](https://pre-commit.com) 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: + +```python +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: + +```python +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: + +```python +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: + +```python +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](#check-functions) +- `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](#check-functions). + +### `RequiredRecurse(key, schema)` + +Similar to `Required`, but uses a [schema](#making-a-schema). + +### `Optional(key, check_fn, default)` + +If a key is present, check that it adheres to the +[check function](#check-functions). + +- `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](#making-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](#check-functions). +- 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](#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](#making-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: + +```python +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: + +```python +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: + +```python +Required('args', check_array(check_string)) +``` + +### `check_and(*fns)` + +Returns a function that performs multiple checks on a value. + +For example: + +```python +Required('language', check_and(check_string, my_check_language)) +``` |