Loading Data

Defining Your Schema

Considering the html/css model, a Schema lines up with an element (such as a <p> or <div>). Once you define the Schemas that make up your config, elements in your settings will be mapped against the schema. An HTML document may look like

<body>
        <div class="outer">
                <p>Hello!</p>
        </div>
</body>

In that example the elements are body, div, and p. In SettingsCascade you define the elements that make sense for your app. Imagine a task runner app like Fabric. You may have the concept of Tasks and Environments.

from settingscascade import SettingsSchema

class Environment(SettingsSchema):
        _name_ = "env"
        python: str
        pythonpath: List[str]

class Task(SettingsSchema):
        _name_ = "task"
        command: List[str]
        wait: bool

A SettingsSchema class has one mandatory field- _name_. This is the name of the element as it will appear in setting files. It is the equivalent of div or p. Each schema defines annotations for the valid variables that make up a config for that element. When you load data, if the rule has an element defined, the system will do some validation of the type of the data you are loading. This is not a comprehensive check- for example, if you define List[str] this will only verify that the data is a list, it will not look at the values. typing.Any can be used as expected.

If you wanted to create a Schema that will check settings defined on it, but allow rules that aren’t defined instead of throwing a ValueError, add the _allowextra_ property to the class.

AnyValue(ElementSchema):
        _allowextra_ = True

Specificity

When loading data, each section of rules will be associated with a selector, and then when your app tries to look up a rule, the most specific rule whose selector matches the current context will be returned. Consider

".env":
        val_a = "a"

"class.env":
        val_a = "b"

If you try to look up val_a from the context module.env it would return “a”, while from context class.env it would return “b”. A rule section must match ALL elements of the context to be used, but not all elements of the context need to be used in the selector. (The first example would work because there is no rule that matches module.env, but there IS a rule that matches *.env) The specificity rules are the same as CSS- the score is a 3-tuple- - Count of #ID values - Count of .class values - count of element values.

So- - .myclass == 0, 1, 0 - el.myclass == 0, 1, 1 - parent child#thechild == 1, 0, 2

Specificity scores are compared pairwise, the first value, then the second, then the third. The three examples above are listed from least specific to most.

Data Loader

The core class of SettingsCascade it the SettingsManager class. You create a settings manager by passing it a list of data dictionaries and a list of ElementSchema classes that you have defined. It will then build its internal cascade of rule definitions (verified according to the schemas you passed). The dictionaries themselves can be created any way you want- load from TOML, JSON, Yaml, a Python dict, whatever.

from pathlib import Path
from toml import loads
from settingscascade import SettingsManager

els = {EnvironmentSchema, TaskSchema}
data =  loads(Path("pyproject.toml").read_text())
default_data = {"mydefault": 42}
config = SettingsManager([default_data, data], els)

The algorithm for loading the data for each rule section is 1. Determine the selector term for this section. 2. Check for any key named _name_ - add it to the selector as a class. 3. check for any key named _id_ - add it to the selector as an id. 4. Append the selector to the context passed in from the parent to get the full selector for this section. 5. Iterate through the remaining key, value pairs. 6. If the key matches the _name_ of one of the element schemas or contains a . or #, load that value as a new section, passing the selector as the current context and the key as that sections selector term. 7. For each remaining key, if this section matches an element schema, verify that the key is in the annotations for the schema and the value has the correct type. 8. Load the key, value pairs into the config manager as a Rules section.

There are two ways to add classes or ids to a rule section selector. first, you can just add them directly as though it were css. The toml file below has four sections. The specifiers are read as environment, .prod, environment.prod, environment.prod task. This winds up working exactly like CSS, and is the most obvious way to use this library.

[environment]
setting_a = "outer"

[".prod"]
some_setting = "production"

["environment.prod"]
        name = "default_task"
        task_setting = "less"
["environment.prod".task]
        setting_a = "inner"

You’ll notice that in toml, to put a . or a # in the key of a section, you’ll have to use quotes. Because of that, the loader will also look for magic names _name_ and _id_ to pull them from the object. Below, the selector for section 2 would be tasks.default_task

[environment]
setting_a = "outer"

[[task]]
        _name_ = "default_task"
        task_setting = "less"
        [task.environment]
                setting_a = "inner"

[[task]]
task_setting = "more"

Note there are two special cases to consider from the previous example. The first is a list of dictionaries (like task). In this case the library will use the key of the list to build the selector for each element of the list. In this case it would be task.default_task and task respectively. The other is that the second list there has no _name_ variable, so will just get the selector from the list- if there were more then one item in that list with the same situation, they would override each other. In the event that two rule section have the SAME specificity, they get priority in reverse order of how they were loaded- the last section beats the first.

Detailed config to selector rules

Parse map into selector/ruleset ! Any key that is not an element is considered to be a rule ! There are two more special keynames - _name_ and _id_. If these are contained in a map, they update the selector of the parent key

keyname “keyname”: {…

keyname.typename “keyname.typename”: {…

keyname.typename “keyname”: {“_name_”: “typename, …

keyname#id “keyname”: {“_id_”: “id”, …

keyname.typename “keyname”: [{“_name_”: “typename”, …

keyname otherkeyname.nestedname “keyname”: {“otherkeyname”: {“_name_”: “nestedname”, …