versionedconfig 0.1.0
Easy versioned JSON config files
License: Apache 2.0

Versioned config file class for python 3x
=========================================

This package provides an easy way to define nested configuration data structures
in python, save/load them to/from JSON files, and migrate older versions of your config
file to the latest version.

See `API documentation <https://eriknyquist.github.io/versionedconfig/>`_

Config file example
-------------------

.. code:: python

    from versionedconfig.config import VersionedConfig

    # Nested config object
    class DisplayConfig(VersionedConfig):
        display_mode = "windowed"
        resolution = "1920x1080"
        volume = 0.66

    # Top-level config object with another nested config object
    class UserConfig(VersionedConfig):
        version = "v1.0.0"
        username = "john smith"
        friend_list = ["user1", "user2", "user3"]
        display_config = DisplayConfig

    # Change some values
    UserConfig.display_config.volume = 1.0
    UserConfig.username = "jane doe"

    # Save to JSON file
    UserConfig.to_file('user_config.json', indent=4)

    # Load from JSON file
    UserConfig.from_file('user_config.json')


You can also just get the config data as a string:

::

    >>> UserConfig.to_json(indent=4)

    {
        "version": "v1.0.0",
        "username": "jane doe",
        "friend_list": [
                "user1",
                "user2",
                "user3"
        ],
        "display_config": {
            "display_mode": "windowed",
            "resolution": "1920x1080",
            "volume": 1.0
        }
    }

Migrations -- making use of the version number
----------------------------------------------

Any VersionedConfig object can have a ``version`` attribute, which can be any object,
although it is typically a string (e.g. ``"v1.2.3"``). This version attribute can be
used to support migrations for older config files, in the event that you need to
change the format of your config file.

Let's take the same config file definition from the previous example:

.. code:: python

    from versionedconfig.config import VersionedConfig

    # Nested config object
    class DisplayConfig(VersionedConfig):
        display_mode = "windowed"
        resolution = "1920x1080"
        volume = 0.66

    # Top-level config object with another nested config object
    class UserConfig(VersionedConfig):
        version = "v1.0.0"
        username = "john smith"
        friend_list = ["user1", "user2", "user3"]
        display_config = DisplayConfig

Imagine we've already released this code out into the world. People are already
using it, and they have JSON files generated by the ``UserConfig`` class sitting
on their computers.


Now, imagine you are making a new release of your software, and some new features
require you to make the following changes to the config file structure:

* remove the the ``DisplayConfig.resolution`` field entirely
* change the name of ``DisplayConfig.volume`` to ``DisplayConfig.volumes``
* change the value of ``DisplayConfig.volumes`` from a float to a list

.. code:: python

    from versionedconfig.config import VersionedConfig

    # Nested config object
    class DisplayConfig(VersionedConfig):
        display_mode = "windowed"
        # 'resolution' field is deleted
        volumes = [0.66, 0.1] # 'volume' is now called 'volumes', and is a list

    # Top-level config object with another nested config object
    class UserConfig(VersionedConfig):
        version = "v1.0.0"
        username = "john smith"
        friend_list = ["user1", "user2", "user3"]
        display_config = DisplayConfig

Now, if you send this updated UserConfig class to your existing users, it will fail
to load their existing JSON files with version ``v1.0.0``, since those files will contain
the ``DisplayConfig.resolution`` field that we deleted in ``v1.0.1``, and
``DisplayConfig.volume`` will similarly be gone, having been replaced with
``DisplayConfig.volumes``. This situation is what migrations are for.

The solution is to:

#. Change the version number to something new, e.g. ``v1.0.0`` becomes ``v1.0.1``
#. Write a migration function to transform ``v1.0.0`` config data into ``v1.0.1`` config data

.. code:: python

    from versionedconfig.config import VersionedConfig

    # Nested config object
    class DisplayConfig(VersionedConfig):
        display_mode = "windowed"
        # 'resolution' field is deleted
        volumes = [0.66, 0.1] # 'volume' is now called 'volumes', and is a list

    # Top-level config object with another nested config object
    class UserConfig(VersionedConfig):
        version = "v1.0.1" # Version has been updated to 1.0.1
        username = "john smith"
        friend_list = ["user1", "user2", "user3"]
        display_config = DisplayConfig

    # Create the migration function for v1.0.0 to v1.0.1
    def migrate_100_to_101(attrs):
        del attrs['display_config']['resolution']        # Delete resolution field
        del attrs['display_config']['volume']            # Delete volume field
        attrs['display_config']['volumes'] = [0.66, 0.1] # Add defaults for new volume values
        return attrs                                     # Return modified data (important!)

    # Add the migration function for v1.0.0 to v1.0.1
    UserConfig.add_migration("v1.0.0", "v1.0.1", migrate_100_to_101)

Now, JSON files with version ``v1.0.0`` will be migrated to version ``v1.0.1``, and when you do
``UserConfig.to_json`` or ``UserConfig.to_file``, the new migrated version will be written
out.

The downside to this approach, is that you have to manually bump the version number,
and write a new migration function, anytime the structure of your config data changes.

The upside, of course, is that you can relatively easily support migrating any older
version of your config file to the current version.

If you don't need the versioning/migration functionality, just never change your version
number, or don't create a ``version`` attribute on your ``VersionedConfig`` classes.