configfile: Application saved settings support

Tools typically do not use this module directly; they instead use the chimerax.core.settings module, which layers additional capabilities on top of this module’s ConfigFile class.

This module provides support for accessing persistent configuration information, a.k.a., saved settings. The information is stored in a file in a human-readable form, *i.e., text, but is not necessarily editable.

The configuration information is considered to be an API and has a semantic version associated with it.

Configuration information is kept in properties. And those properties have names and values.

Each tool has its own settings. The MAJOR part of the semantic version is embedded in its filename. For the ChimeraX core, that version does not change during the life of the release, so patches may only introduce additional information. Tools are allowed to change and might or might not implement backwards compatibility.

Accessing Configuration Information

Access Tool Configuration:

settings = tool.get_settings()
if settings.PROPERTY == 12:  # access a value
    pass
settings.PROPERTY = value    # set a value

Access ChimeraX Core Configuration:

from chimerax.core.core_settings import settings
# (ibid)

Declaring the Configuration API

The fact that there are configuration files is hidden by an object that implements the tool’s configuration API.

Most tools will only have one section. So the ConfigFile and Section subclasses (next example) can be combined into one:

_config = None

BlastMatrixArg = cli.EnumOf((
    'BLOSUM45', 'BLOSUM62', 'BLOSUM80', 'PAM30', 'PAM70'
))

class _BPPreferences(configfile.ConfigFile):

    PROPERTY_INFO = {
        'e_exp': configfile.Value(3, cli.PositiveIntArg, str),
        'matrix': configfile.Value('BLOSUM62', BlastMatrixArg, str)
        'passes': 1,
    }

    def __init__(self, session):
        ConfigFile.__init__(self, session, "Blast Protein")

def get_preferences():
    global _prefs
    if _prefs is None:
        _prefs = _BPPreferences()
    return _prefs

# reusing Annotations for command line arguments
@cli.register("blast",
    cli.CmdDesc(
        keyword=[('evalue', cli.PostiveIntArg),
                 ('matrix', BlastMatrixArg),]
))
def blast(session, e_exp=None, matrix=None):
    prefs = get_preferences()
    if e_exp is None:
        e_exp = prefs.e_exp
    if matrix is None:
        matrix = prefs.matrix
    # process arguments

Property values can either be a Python literal, which is the default value, or a Value has three items associated with it:

  1. A default value.
  2. A function that can parse the value or a cli Annotation that can parse the value. This allows for error checking in the case where a user hand edits the configuration.
  3. A function to convert the value to a string.

If the tool configuration API changes, then the tool can subclass Preferences with custom code.

Adding a Property

If an additional property is needed, just add it the PROPERTY_INFO class attribute, and document it. The minor part of the version number should be increased before the tool is released again. That way other tools can use the tool’s version number to tell if the property is available or not.

Renaming a Property

Since configuration is an API, properties can not be removed without changing the major version number. To prepare for that change, document that the old name is deprecated and that the new name should be used instead. Then add a Python property to the section class that forwards the changes to the old property name. For example, to rename e_exp, in the previous example, to e_value, extend the _Params class with:

class _Params(configfile.Section):

    PROPERTY_INFO = {
        'e_exp': ( cli.PositiveIntArg, str, 3 ),
        'matrix': ( BlastMatrixArg, str, 'BLOSUM62' )
    }

    @property
    def e_value(self):
        return 10 ** -self.e_exp

    @e_value.setter
    def e_value(self, value):
        import math
        self.e_exp = -round(math.log10(value))

Later, when the major version changes, the existing ConfigFile subclass would be renamed with a version suffix with the version number hardcoded, and a new subclass would be generated with the e_exp replaced with e_value. Then in the new ConfigFile subclass, after it is initialized, it would check if its data was on disk or not, and if not, try opening up previous configuration versions and migrate the settings. The migrate_from methods, ConfigFile.migrate_from() and Section.migrate_from(), may be replaced or can be made more explicit. See the next section for an example.

Changing the API - Migrating to a New Configuration

Migrating Example:

class _BPPreferences(configfile.ConfigFile):

    PROPERTY_INFO = {
        'e_exp': ( cli.PositiveIntArg, str, 3 ),
        'matrix': ( BlastMatrixArg, str, 'BLOSUM62' )
    }

    # additional properties removed

    def __init__(self, session):
        ConfigFile.__init__(self, session, "Blast Protein")


class _BPPreferences(configfile.ConfigFile):

    PROPERTY_INFO = {
        'e_value': ( float, str, 1e-3 ),
        'matrix': ( BlastMatrixArg, str, 'BLOSUM62' )
    }

    # e_exp is gone

    def migrate_from(self, old, version):
        configfile.Section.migrate_from(self, old, version)
        self.e_value = 10 ** -old._exp

    def __init__(self, session):
        # add version
        ConfigFile.__init__(self, session, "Blast Protein", "2")
        if not self.on_disk():
            old = _BPPreferences()
            self.migrate_from(old, "1")

Migrating a Property Without Changing the Version

This is similar to renaming a property, with a more sophisticated getter function:

class _Params(configfile.ConfigFile):

    PROPERTY_INFO = {
        'e_value': 1e-3,
        'matrix': configfile.Value('BLOSUM62', BlastMatrixArg, str)
    }

    @property
    def e_value(self):
        def migrate_e_exp(value):
            # conversion function
            return 10 ** -value
        return self.migrate_value('e_value', 'e_exp', cli.PositiveIntArg,
                                   migrate_e_exp)

The migrate_value() function looks for the new value, but if it isn’t present, then it looked for the old value and migrates it. If the old value isn’t present, then the new default value is used.

class ConfigFile(session, tool_name, version='1')

Bases: object

In-memory handle to persistent configuration information.

Parameters:

session : Session

(for logger)

tool_name : the name of the tool

version : configuration file version, optional

Only the major version part of the version is used.

Attributes

PROPERTY_INFO (dict of property_name: value) property_name must be a legal Python identifier. value is a Python literal or an Item.
filename

The name of the file used to store the settings

migrate_from(old, version)

Migrate identical settings from old configuration.

Parameters:

old : instance Section-subclass for old section

version : old version “number”

migrate_value(name, old_name, old_from_str, convert)

For migrating property from “.old_name” to “.name”.

First look for the new value, but if it isn’t present, then look for the old value and migrate it. If the old value isn’t present, then the new default value is used.

Parameters:

name : current name of property

old_name : previous name of property

old_from_str : function to parse previous version of property

convert : function to old value to new value

on_disk()

Return True the configuration information was stored on disk.

This information is useful when deciding whether or not to migrate settings from a previous configuration version.

reset()

Revert all properties to their default state

save()

Save configuration information of all sections to disk.

Don’t store property values that match default value.

update(dict_iter=None, **kw)

Update all corresponding items from dict or iterator or keywords.

Treat preferences as a dictionary and update() them.

Parameters:

dict_iter : dict/iterator

**kw : optional name, value items

class Value(default, from_str=None, to_str=None)

Bases: object

Placeholder for default value and conversion functions

Parameters:

default : is the value when the property has not been set.

from_str : function or Annotation, optional

can be either a function that takes a string and returns a value of the right type, or a cli Annotation. Defaults to py:func:ast.literal_eval.

to_str : function returning a string, optional

Defaults to repr().

Attributes

section (Section instance)