Skip to content

Commit

Permalink
docs: Wrote user docs, edit docstrings for .rst syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
stuarteberg committed Jan 1, 2019
1 parent ce64bc8 commit 53a8cee
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 40 deletions.
93 changes: 65 additions & 28 deletions confiddler/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,31 @@
_Error.__hash__ = object.__hash__


def load_config(path_or_file, schema={}):
def load_config(path_or_file, schema={}, inject_defaults=True):
"""
Convenience wrapper around :py:func:`validate()`.
(This function accepts a file).
Load the config data from the given file (or path to a file),
and validate it against the given schema.
All missing values will be inserted from schema defaults.
If a setting is missing and the schema contains no default
value for it, a ValidationError is raised.
value for it, a ``ValidationError`` is raised.
Note:
If your config data is already loaded into a dict and
you just want to validate it and/or inject defaults,
see ``validate()``.
see :py:func:`validate()`.
Args:
path_or_file:
The raw config data. Either a file object or a file path.
schema:
The config schema, already loaded into a Python dict.
The config schema, already loaded into a Python ``dict``.
Returns:
dict
``dict``
"""
assert isinstance(schema, Mapping), \
"Invalid schema type: should be a dict of jsonschema specs"
Expand All @@ -54,7 +57,7 @@ def load_config(path_or_file, schema={}):

def _load_config(f, schema):
config = yaml.load(f)
validate(config, schema, inject_defaults=True)
validate(config, schema, inject_defaults=inject_defaults)
return config

if isinstance(path_or_file, PathLike):
Expand All @@ -66,24 +69,26 @@ def _load_config(f, schema):

def dump_default_config(schema, f=None, format="yaml"): #@ReservedAssignment
"""
Convenience wrapper around :py:func:`emit_defaults()`.
(This function writes to a file).
Dump the default config settings from the given schema.
Settings without default values will use '{{NO_DEFAULT}}' as a placeholder.
Settings without default values will use ``"{{NO_DEFAULT}}"`` as a placeholder.
Args:
schema:
The config schema
f:
File object to which default config data will be dumped.
If None, then the default config is returned as a string.
If ``None``, then the default config is returned as a string.
format:
Either "json", "yaml", or "yaml-with-comments".
The "yaml-with-comments" format inserts comments above each setting,
populated with the setting's "description" field from the schema.
Either ``"json"``, ``"yaml"``, or ``"yaml-with-comments"``.
The ``"yaml-with-comments"`` format inserts comments above each setting,
populated with the setting's ``"description"`` field from the schema.
Returns:
None, unless no file was provided, in which
``None``, unless no file was provided, in which
case the default config is returned as a string.
"""
assert format in ("json", "yaml", "yaml-with-comments")
Expand All @@ -108,22 +113,22 @@ def emit_defaults(schema, include_yaml_comments=False, yaml_indent=2, base_cls=N
"""
Emit all default values for the given schema.
Similar to calling validate({}, schema, inject_defaults=True), except:
Similar to calling ``validate({}, schema, inject_defaults=True)``, except:
1. Ignore schema validation errors and 'required' property errors
2. If no default is given for a property, inject '{{NO_DEFAULT}}',
2. If no default is given for a property, inject ``"{{NO_DEFAULT}}"``,
even if the property isn't supposed to be a string.
3. If include_yaml_comments is True, insert CommentedMap objects instead of ordinary dicts,
and insert a comment above each key, with the contents of the property "description" in the schema.
3. If ``include_yaml_comments`` is True, insert ``CommentedMap`` objects instead of ordinary dicts,
and insert a comment above each key, with the contents of the property ``"description"`` in the schema.
Args:
schema:
The schema data to pull defaults from
include_yaml_comments:
Whether or not to return ruamel.yaml-compatible dicts so that
Whether or not to return ``ruamel.yaml`` objects so that
comments will be written when the data is dumped to YAML.
yaml_indent:
Expand Down Expand Up @@ -173,14 +178,15 @@ def is_array(checker, instance):

def validate(instance, schema, base_cls=None, *args, inject_defaults=False, **kwargs):
"""
Drop-in replacement for jsonschema.validate(), with the following extended functionality:
Drop-in replacement for ``jsonschema.validate()``,
with the following extended functionality:
- Specifically allow types from ruamel.yaml.comments
- If inject_defaults is True, this function *modifies* the instance IN-PLACE
- Specifically allow types from ``ruamel.yaml.comments``
- If ``inject_defaults`` is ``True``, this function *modifies* the instance IN-PLACE
to fill missing properties with their schema-provided default values.
See the jsonschema FAQ:
http://python-jsonschema.readthedocs.org/en/latest/faq/
See the `jsonschema FAQ <http://python-jsonschema.readthedocs.org/en/latest/faq>`_
for details and caveats.
"""
if base_cls is None:
base_cls = validators.validator_for(schema)
Expand Down Expand Up @@ -335,13 +341,44 @@ def _set_default_object_properties(properties, instance, include_yaml_comments,

def flow_style(ob):
"""
Convert the object into its corresponding ruamel.yaml subclass,
to alter the yaml pretty printing behavior for this object.
This function can be used to fine-tune the format of exported YAML configs.
(It is only needed rarely.)
By default, :py:func:`dump_default_config()` uses 'block style':
This allows us to print default configs in yaml 'block style', except for specific
values (e.g. int sequences), which look nicer in 'flow style'.
.. code-block:: python
(For all other uses, the returned ob still looks like a list/dict/whatever)
>>> schema = {
"properties": {
"names": {
"default": ['a', 'b', 'c']
}
}
}
>>> dump_default_config(schema, sys.stdout)
names:
- a
- b
- c
But if you'd prefer for a particular value to be written with 'flow style',
wrap it with ``flow_style()``:
.. code-block:: python
>>> from confiddler import flow_style
>>> schema = {
"properties": {
"names": {
"default": flow_style(['a', 'b', 'c'])
}
}
}
>>> dump_default_config(schema, sys.stdout)
names: [a, b, c]
"""
sio = io.StringIO()
yaml.dump(ob, sio)
Expand Down
17 changes: 15 additions & 2 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.autodoc',
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.doctest',
'sphinx.ext.napoleon',
'sphinx.ext.coverage',
'sphinx.ext.viewcode',
'sphinx.ext.githubpages']
Expand Down Expand Up @@ -87,7 +89,18 @@
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
#html_theme = 'alabaster'

# only import and set the theme if we're building docs locally
# otherwise, readthedocs.org uses their theme by default, so no need to specify it
# (on_rtd is whether we are on readthedocs.org)
import os
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if not on_rtd:
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]


# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
Expand Down
31 changes: 31 additions & 0 deletions docs/source/core.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.. currentmodule:: confiddler

.. _core:


API Reference
=============

..
(The following |br| definition is the only way
I can force numpydoc to display explicit newlines...)
.. |br| raw:: html

<br />


..
- :py:func:`load_config <load_config>`

.. autofunction:: load_config
.. autofunction:: dump_default_config

.. autofunction:: validate
.. autofunction:: emit_defaults


..
.. autofunction:: flow_style
54 changes: 44 additions & 10 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,52 @@
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to confiddler's documentation!
======================================
.. |br| raw:: html

<br />

confiddler
==========

``confiddler`` has tools to help you define and load YAML configuration files.


Here's the basic idea:

1. Define your config file structure with a JSON schema.
(See `json-schema.org <https://json-schema.org>`_ or
`jsonschema <http://python-jsonschema.readthedocs.org>`_).



2. Help your users get started by showing them a **default config file** which is:

- auto-generated from your defaults, and
- auto-commented with your schema ``description``.

.. toctree::
:maxdepth: 2
:caption: Contents:

3. Load a user's config file with :py:func:`confiddler.load_config()`, which will:

- validate it against your schema
- auto-inject default values for any settings the user omitted

See the :ref:`Quickstart <quickstart>` for a short example.


Install
-------

.. code-block:: bash
conda install -c flyem-forge confiddler
Contents
--------

.. toctree::
:maxdepth: 2

Indices and tables
==================
quickstart
core

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
64 changes: 64 additions & 0 deletions docs/source/quickstart.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
.. _quickstart:


Quickstart
----------

Define your schema

.. code-block:: python
>>> from confiddler import dump_default_config, load_config
>>> schema = {
"description": "Settings for a robot vacuum cleaner",
"properties": {
"speed": {
"description": "Speed of the robot (1-10)",
"type": "number",
"minValue": 1,
"maxValue": 10,
"default": 1
},
"movement": {
"description": "Movement strategy: random, raster, or spiral",
"type": "string",
"enum": ["random", "raster", "spiral"],
"default": "random"
}
}
}
Show your user the default config.

.. code-block:: python
>>> dump_default_config(schema, sys.stdout, 'yaml')
speed: 1
movement: random
>>> dump_default_config(schema, sys.stdout, 'yaml-with-comments')
#
# Settings for a robot vacuum cleaner
#
# Speed of the robot (1-10)
speed: 1
# Movement strategy: random, raster, or spiral
movement: random
Load your user's config.

.. code-block:: yaml
# my-robot-config.yaml
speed: 2
.. code-block:: python
>>> load_config('my-robot-config.yaml', schema, inject_defaults=True)
{'speed': 2, 'movement': 'random'}

0 comments on commit 53a8cee

Please sign in to comment.