Configuration management

CommandGraph Commands are Configurable objects, which means they can be constructed from JSON-like objects and support configuration schema specification (to document and validate configuration fields).

Non-command configurable objects can be defined as well, which can be useful when components are shared between multiple commands:

class Muppet(cg.Configurable):
  ...

kermit = Muppet(color='green', has_it_easy=False)

Configurable object properties

An object’s configuration can be accessed via obj.conf. obj.spec provides its specification: its configuration augmented with a field indicating its type.

kermit.conf # => Namespace(color='green', has_it_easy=False)
kermit.spec # => Namespace(color='green', has_it_easy=False, type='__main__|Muppet')

Creating objects from specifications

Objects can be instantiated from specifications using the create function. This can be helpful when instantiating configurable objects within commands.

class PutOnAShow(cg.Command):
  def run(self):
    muppet = cg.create(self.conf.muppet)
    print(muppet.tell_a_joke())

PutOnAShow(muppet=load_muppet_spec())()

Defining scopes

By default, the type field in an object’s specification is derived from it’s type’s name and module path, which may be volatile over the course of a project’s development. This limits the usefulness of stored specifications.

Entering a Scope can override this default behavior with more stable (and often more readable) bindings:

with cg.Scope({'Muppet': a.b.c.Something}):
  muppet = cg.create({'type': 'Muppet'}) # => <a.b.c.Something instance>
  muppet.spec # => Namespace(type='Muppet')

Todo

Fix the “conflicting meanings of namespace” issue. Maybe types.SimpleNamespace should be dropped in favor of dicts? Maybe cg.Namespace should be called cg.Scope?

Defining schemas

Override a configurable type’s Conf class to specify a configuration schema.

Members of Conf are interpreted in the following way:

  • The member’s name corresponds to the expected property’s name.
  • A type value specify the property’s expected type.
  • A single-element list value specifies the property’s default value.
  • A str value specifies the property’s docstring.
  • A dict value specifies constraints in raw JSON-Schema.
  • A tuple value may specify any combination of the above.

Example:

class Person(cg.Configurable):
  class Conf:
    name = str, 'a long-winded pointer'
    age = int, [0], 'solar rotation count'
    favorite_color = {'enum': ['blue', 'green', 'other']}
    shoe_size = 'European standard as of 2018-08-17'

Defining configuration schemas is completely optional, but it enables configuration validation and provides helpful documentation, both in the code, and in CommandGraph-generated web and command-line interfaces.

Generating a command-line interface

cli generates a command-line interface exposing every command in the current scope stack.

run-cmd:

#!/usr/bin/env python3
# Generates the interface
#   `<this-file> [<cmd-spec>]`.
with cg.Scope({'a': DoA, 'b': DoB}):
  cg.cli()

Command line:

> ./run-cmd {type: a, mannerInWhichToA: relentlessly}

If there is exactly one Command in the scope stack, the “type” field in the command specification can be omitted.

run-a:

#!/usr/bin/env python3
with cg.Scope({'a': DoA}):
  cg.cli()

Command line:

> ./run-a {other: a, options: here}