Features

ModCFG features the following:

Note

In this documentation, there will be several codeblocks like

ModCFG document
...
Python script
...

and

Expected output
...

Please interpret the code in ModCFG Document as the DOC variable given in the Python scripts.

Enumerations

Let’s say you have an enum MyEnum defined as

import enum
class MyEnum(enum.Enum):
    is_cool = "we are swag"
    isnt_cool = "we are not swag"

And we want our clients to use that enum somewhere in the configuration format. Like this (in YAML format):

Example YAML file
bob:
    personality: is_cool  # Pretend this is the enumeration
    hair color: brown
    loves yaml: no

Before we look at ModCFG, let’s check out some other ways to implement an enumeration system.

JSON

JSON has no built-in support for enums so you’ll need to turn to post-processors like JSONSchema.

YAML, TOML, INI, etc

The same goes for YAML, TOML, INI, etc. They all don’t have built-in enumeration support. You still have to turn to post-processing in order to implement an enum system.

ModCFG

But it’s really easy to do with ModCFG’s built-in enumeration support:

ModCFG document
module "Bob":
    personality: :is_cool
    'hair color': brown
    'loves yaml': no
Python script
 import modcfg, enum


 class MyEnum(enum.Enum):  # Define the enum
     is_cool = "we are swag"
     isnt_cool = "we are not swag"

 output = modcfg.loads(
     DOC,
     enums=[MyEnum],  # Use the enum
 )
 print(output)
Expected output
[
    Module(
        name="Bob",
        contents={
            "personality": <MyEnum.is_cool: 'we are swag'>,
            "hair color": "brown",
            "loves yaml": "no",
        },
    )
]

Epic, right? ModCFG has an exhaustive enumeration resolver which never fails silently unless explicitly requested so.

Here’s another example:

ModCFG document
module ThatXliner:
    personality = :is_cool
    hair_color => black
    coder = true
module SomePythoniast:
    hates = :polymorphism
    loves = :duck_typing
Python script
 import modcfg, enum


 class MyFirstEnum(enum.Enum):
     is_cool = "we are swag"
     isnt_cool = "we are not swag"


 class MySecondEnum(enum.Enum):
     polymorphism = "sucks"
     duck_typing = "is cool"

 print(
     modcfg.loads(
         DOC,
         enums=[MyFirstEnum, MySecondEnum],
     )
 )
Expected output
[
    Module(
        name="ThatXliner",
        contents={
            "personality": <MyFirstEnum.is_cool: 'we are swag'>,
            "hair_color": "black",
            "coder": True,
        },
    ),
    Module(
        name="SomePythoniast",
        contents={
            "hates": <MySecondEnum.polymorphism: 'sucks'>,
            "loves": <MySecondEnum.duck_typing: 'is cool'>,
        },
    ),
]

But there are cases were there are ambiguous enums. If that’s the case, ModCFG will, if you didn’t explicitly silence it, fail on ambiguous enums

Ambigous Enumerations

If there’s ambiguous enums, ModCFG’s exhaustive enumeration resolver will raise a modcfg.errors.EnumResolveError like so:

ModCFG document
module Story:
    is_made_by_a: :duck_typing  # NANI!?
mod Python:
    has = :duck_typing
Python script
 import modcfg, enum


 class Enum1(enum.Enum):
     duck_typing = "DUCKS CAN TYPE!!?"
     human_typing = "Much better"


 class Enum2(enum.Enum):
     polymorphism = "sucks"
     duck_typing = "is cool"

 print(
     modcfg.loads(
         DOC,
         enums=[Enum1, Enum2],
     )
 )
Expected output
Traceback (most recent call last):
    ...
modcfg.errors.EnumResolveError: Ambigous enumerations: <Enum1.duck_typing: 'DUCKS CAN TYPE!!?'>, <Enum2.duck_typing: 'is cool'>

Really, there shouldn’t have ambiguous keys.

But if you really need to, you can silence the ambiguity checker by setting the enum_ambiguity_check parameter to False for modcfg.loads(). It’ll return the first found result instead (i.e. making ModCFG guess which one you want, in the order of enums given in the enums argument).

But if you do find the need for enumerations with ambiguous keys and don’t want ModCFG to guess, you can use the enum namespace specifier like so:

ModCFG document
module Story:
    is_made_by_a: :Enum1.duck_typing  # Can ducks type?
mod Python:
    has = :Enum2.duck_typing
Python script
import modcfg, enum


class Enum1(enum.Enum):
    duck_typing = "DUCKS CAN TYPE!!?"
    human_typing = "Much better"


class Enum2(enum.Enum):
    polymorphism = "sucks"
    duck_typing = "is cool"

print(
    modcfg.loads(
        DOC,
        enums=[Enum1, Enum2],
    )
)
Expected output
[
    Module(
        name="Story",
        contents={"is_made_by_a": <Enum1.duck_typing: 'DUCKS CAN TYPE!!?'>},
    ),
    Module(
        name="Python", contents={"has": <Enum2.duck_typing: 'is cool'>},
    ),
]

Modules

ModCFG features serialization of hashable objects called modules. They’re actually NamedTuples that contain 2 slots:

  • name: The name of the module

  • contents: The module contents

The thing is, ModCFG currently doesn’t treat a list of modules like dictionaries where one module with the same name as one defined earlier can overwrite said module defined earlier.

Not required

Instead of limiting ModCFG to a configuration format that requires modules, I decided I wanted it to also be a data format: a YAML-like alternative:

>>> import modcfg
>>> DOC = """
... bruh:
...    yaml: {
...        kinda:
...            - works
...            - {    this: [is,
...
...                insane]
...
...     }
...    }
    """
>>> print(modcfg.loads(DOC))
[{'bruh': {'yaml': {'kinda': ['works', {'this': ['is', 'insane']}]}}}]

Want to remove those unnecessary brackets around it which denotes a list? Set the inline parameter of modcfg.loads() to True. What this does is that if, and only if, the parsed and evaluated values is a one-element list, then it’ll return that one element.

Why is the default a list? That way, one can define multiple modules in a document, like this:

ModCFG document
module "Module one!":
    some: content
mod "Module 2.":
    some: 'more content!'
Python script
import modcfg

print(modcfg.loads(DOC))
Expected output
[
    Module(name="Module one!", contents={"some": "content"}),
    Module(name="Module 2.", contents={"some": "more content!"}),
]

Or even (shudder)

ModCFG document
module "Module one!":
    some: content
[loving]
{weird: stuff}
Python script
import modcfg

print(modcfg.loads(DOC))
Expected output
[
    Module(name="Module one!", contents={"some": "content"}),
    ["loving"],
    {"weird": "stuff"},
]

Datetimes and dates

ModCFG supports datetimes and dates:

ModCFG document
mod 'Date example':
    today = datetime(2021-04-18 14:50:55.016922)
    tomorrow = date(2021-04-19)
Python script
import modcfg
print(modcfg.loads(DOC))
Expected output
[
    Module(
        name="Date example",
        contents=[
            {
                "today": datetime.datetime(2021, 4, 18, 14, 50, 55, 16922),
                "tomorrow": datetime.date(2021, 4, 19),
            }
        ],
    )
]

Strings

Single-line strings can be single quoted (') or double quoted ("). You can leave out the quotes if the string contents matches the following regex:

[a-zA-Z_]\w*

Multiline strings can also be single or double quoted. They are denoted with 2 quote characters.

Both (multiline or single-line) strings may be prefixed with t, l, or r this is what they mean:

t

Dedent the string’s contents (uses textwrap.dedent()).

l

Run str.lstrip() on the string’s contents.

r

Run str.rstrip() on the string’s contents. Very similar to l.

Warning

When prefixing, you must put t, l, or r in that exact order.

Escaping characters

You can create unicode characters using the \uXXXX escape sequence. The u is case-insensitive

Example:

>>> import modcfg
>>> print(modcfg.loads(R'{main: "\U0001f642"}'))
[{'main': '🙂'}]

Future restraints

Key-value separation characters

Currently, I can’t decide whether the key separation character (the : in {hello: world}) should be which character. So I made it the following characters letting you decide.

  • : Good ol’ colon

  • => Fat arrow

  • -> Skinny arrow

  • = Equal sign

I may make a poll in the future.

Module definition keywords

Currently, you can use either mod or module to define a module.

In the future, I plan to limit it to either one or the other.