summaryrefslogtreecommitdiffstats
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md282
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))
+```