Dynamic settings¶
Some settings get their names only at runtime. Examples are a window layout that the user rearranges, dashboards, and options provided by plugins. Such settings need no declared member. They live in the dynamic collection of the plugin that is bound to the settings class, and the entries are stored in whatever storage that plugin uses.
Advexp.Settings is responsible for writing: an entry is written back to its storage through the plugin. The read-only package of the family, Advexp.Configuration, can read a written collection back. It serves the same entries as a read-only snapshot through its own dynamic plugins, without depending on this package (see Reading a written collection below).
By default, the bundled JSON plugin keeps dynamic entries in the same file as the declared (static) members:
import advexp.settings as adv_settings
import advexp.settings.plugins.json_dynamic_settings as jds
class LocalSettings(
adv_settings.Settings,
options=jds.json_source(file="app-settings.json"),
):
host: str = "localhost"
plugin = LocalSettings.get_plugin(jds.JSONDynamicSettingsPlugin)
# this class's dynamic entries from the same file
plugin.load_settings()
# the type of the default is used for conversion
flag = plugin.get_setting("dark_mode", default=False)
plugin.set_setting("dark_mode", True)
plugin.save_settings()
Dynamic entries have their own section of the document, grouped by settings class. Static keys stay at the root:
TOML and YAML plugins¶
The bundled TOML plugin (advexp.settings.plugins.toml_dynamic_settings,
which requires the toml extra) uses the same arrangement in a TOML file.
There, the section is a table. Everything below that is said about the JSON
plugin applies to the TOML plugin in the same way, with the toml_ names
instead of the json_ ones (TOMLDynamicSettingsPlugin and its parameters,
toml_source(dynamic_file=...), toml_setting).
In a TOML file, a static key is stored at the address that its name spells out, and the class part of the section name is nested in the same way. The name of an entry inside the class table is one literal key, because a name that is given at runtime may be any text:
The bundled YAML plugin (advexp.settings.plugins.yaml_dynamic_settings,
which requires the yaml extra) also works in the same way, with the
yaml_ names (YAMLDynamicSettingsPlugin and its parameters,
yaml_source(dynamic_file=...), yaml_setting). Its section is a mapping,
and dates and other native YAML values are stored as they are.
Where entries go in a JSON document¶
Where the section is placed is a layout choice of the backend. Two parameters
control it, and they correspond to name= and path= of JSONSetting:
JSONDynamicSettingsPlugin.dynamic_settings_section_nameis the key of the section. Assign another key to move the section, or assignNoneto use the flat layout: at the document root, a key made of the letterDand the entry's full name, for example"D.app.LocalSettings.dark_mode".dynamic_settings_section_path(optional) places the section deeper in the tree, as a JSON Pointer. For example,"/config"puts it inside theconfigobject.
Both parameters are looked up along the MRO of the settings class, each independently.
A static setting is rejected if its location overlaps the location of the
section. That means that its own key or path= is an ancestor of the
section, is inside it, or is equal to it. This way, static and dynamic
entries cannot collide.
A separate file for dynamic entries¶
The shared file is only the default. JSONDynamicSettingsConfiguration.file_path
gives the dynamic entries a file of their own:
import advexp.settings.plugins.json_dynamic_settings as jds
jds.JSONDynamicSettingsConfiguration.file_path = "dynamic-settings.json"
You can set the file at three levels. The more specific level wins:
- Process-wide: the assignment above.
- Per module: a settings module can declare its own dynamic file. Use
for_this_moduleon the same parameter. For the classes of that module, this wins over the process-wide value, in the same way as on the static side. - Per class: use the
dynamic_file=parameter of the class'sjson_source(...)binding. It wins over the module declaration. This is the same builder that also carries the file of the static members.
from datetime import timedelta
import advexp.settings as adv_settings
import advexp.settings.plugins.json_dynamic_settings as jds
class LocalSettings(
adv_settings.Settings,
options=jds.json_source(dynamic_file="dynamic-settings.json"),
):
session_ttl: timedelta = timedelta(minutes=30)
The file_path property of the plugin instance does the same thing at
runtime: both channels write the same entry of the class's own metadata, so
the binding is inherited along the class chain. A subclass without its own
binding uses the nearest one above it.
If no dynamic file is set at any level (the default), the dynamic entries go into the static file. The static file is the first of these that applies:
- the class's own
json_source(file=...)binding; - the module's
for_this_moduledeclaration; - the process-wide
JSONSettingsConfiguration.file_path.
Notes:
- Switching files does not move entries. Entries that were already saved stay where they were written.
- In separate-file mode, the static file has no dynamic section. The
location of the section is no longer reserved, so a static key named
dynamicis allowed. - If
file_pathnames the same file as the static side, the location of the section is reserved again, because one document can have only one layout.
Dynamic names: name_pattern¶
The form of dynamic names is set at the module level, on the dynamic side:
JSONDynamicSettingsConfiguration.name_pattern. It determines two names of
an entry:
- the class key in the section;
- in the flat layout, the full name with the
Dprefix at the root.
The default is the full pattern, built from the same slots the static keys
use: {NamespaceName}, {ClassName} and {FieldName} joined by
{Delimiter}. The pattern must contain {FieldName} and a class part.
Without a class part, the section has nothing to group the entries by, and
only the flat layout works.
This pattern does not control the keys of static members. Their form comes
from the json_source(name_pattern=...) of the static family.
Changing the pattern does not move entries that are already stored. They stay under the spelling that they were written with. Therefore, choose the form before the first save, or move the entries by hand.
import advexp.settings.plugins.json_dynamic_settings as jds
jds.JSONDynamicSettingsConfiguration.name_pattern = (
"{ClassName}.{FieldName}"
)
# the section now groups the entries under "LocalSettings"
Using the dynamic collection¶
- The collection lives in memory. Nothing is read from the storage until
load_settings()runs, and nothing is written back untilsave_settings()runs. - Called without
default,get_settingreturns the value in its raw storage form, without type conversion. It raisesKeyErrorif the name is missing. - The plugin behaves like a dictionary: it supports
len(plugin),"name" in plugin,plugin.to_dict(), andplugin.update_from_dict(). - The iteration order is declared in the class metadata under the key
"dynamic_settings_order". TheITERATION_ORDER_METADATAconstant of the plugin holds this name. The listed names come first, followed by the other names in the order in which they were added. - Class-level calls such as
LocalSettings.load_settings()load only the declared members. To work with the dynamic collection, use the plugin explicitly.
Reading a written collection¶
Advexp.Configuration, the read-only package of the family, can read a dynamic
collection that was written by this package. A configuration class declares
the group of the writing class next to its source binding. The read-only
dynamic plugin then serves the entries as a snapshot, which an explicit
load_configuration() call refreshes.
The reading side never writes. Every method that would modify the collection
raises TypeError, which is the same protection that applies to the declared
members in that package.
from datetime import timedelta
import advexp.configuration as adv_config
import advexp.configuration.plugins.json_dynamic_settings as jds
import advexp.configuration.plugins.json_settings as js
# the writing side: the class "app.LocalSettings" of the advexp.settings
# package keeps its dynamic entries in app-settings.json
class DashboardConfig(
adv_config.Configuration,
options=js.json_source(
file="app-settings.json", dynamic_of="app.LocalSettings"
),
):
# a declared member, read from the same file as usual
session_ttl: timedelta = js.json_setting(default=timedelta(minutes=30))
dashboard = DashboardConfig.get_plugin(jds.JSONDynamicSettingsPlugin)
dashboard.load_configuration() # this group's entries from the file
flag = dashboard.get_setting("dark_mode", default=False)
dynamic_of names the group under which the writer's entries are stored. For
a writer class with the default naming, this is "app.LocalSettings". A
writer with its own dynamic name_pattern groups its entries under the class
part of that pattern.
The reading plugin has the same section parameters that this page describes.
If the writer moved the section or switched to the flat layout, set the same
values on the reading side before calling load_configuration(). The file
levels mirror those of the writer (dynamic_file=, the module declaration,
and the process-wide value). The default is the shared file, which is the
file= of the class's json_source(...).
The stored form is a contract between the two packages: a file that is
written by one package can be read by the other. For this reason, the
layouts described above are frozen public formats, not a private choice of
this package. The reading side is described in full in the
Advexp.Configuration overview.
The sample samples/both_packages/dynamic_exchange.py walks through the
whole round trip, in both layouts and both formats.