Skip to content

Settings API

The application-facing API of the settings package.

advexp.settings.Settings

Base class for typed application settings.

All operations are class-level: the settings class itself is the singleton. Instances are supported for reads and writes, but they share the class storage. :meth:to_object and :meth:update_from_object copy values out to a detached instance of the class and back from it, and :meth:to_dict and :meth:update_from_dict do the same with a plain dict.

Every method that addresses one setting by name (the single-setting passes and the setting-metadata methods) also accepts a getter lambda that selects the member: save_setting(lambda s: s.port). The lambda is inspected, never called.

load_settings() classmethod

Load all declared settings from their storages and report the fallbacks.

The behavior matches :meth:advexp.configuration.Configuration.load_configuration: both use the one load pass of the family.

Every member is loaded, whatever happened to the previous one. A member declared :data:~advexp.settings.REQUIRED whose storage provides nothing usable records an error. The error names the cause (no value, a value that cannot be converted, a constraint violation, or a failed storage), the member, its setting, and the class. An optional member (one declared with a value, with NO_DEFAULT, or without default=) records the same error, but it stands at its default instead.

A plain miss is not an error. For an optional member, a miss is the normal way in which the default gets used, so it is not reported at the usual log levels; a debug record names the address that came up empty. When the pass aborts, the optional misses are attached to the abort in the aggregate's misses, so the reader of the failure can see which optional members stood at their defaults.

At the end, one abort reports every required failure at once. It is always :class:~advexp.settings.AggregateLoadError, however many members failed. Its errors tuple holds each member's own error, and its fallbacks tuple holds the optional fallbacks of the same run. If there is no required failure, there is no abort: the method returns the fallbacks as its report (an empty tuple when every member loaded cleanly), so a caller can learn what fell back without parsing logs. A user-facing storage condition (a corrupt or inaccessible storage) does not wait for the end: if the failing member has a default that would mask it, the condition propagates and aborts the whole pass immediately.

The pass builds the class state from scratch and applies it in one step. A completed load fully determines the state: every member takes either the value that its storages produced or its declared default. A value from an earlier load therefore never survives when the current storages hold nothing for that member, so one class can be reused across loads, and each pass stands on its own. A pass that ends in the aggregate abort, or in a propagating storage condition, leaves the class exactly as the call found it: no values are partly applied, and no defaults are served by the failed run. So if a reload fails, the process keeps the configuration that it is already running with.

save_settings() classmethod

Save the current values of all declared settings.

Ordinary backend errors are logged and skipped. A user-facing error (a broken storage) propagates and aborts the whole pass, so the settings declared after the failing one (including those of other backends) stay unsaved.

delete_settings() classmethod

Delete all declared settings from their storages and reset them to their defaults.

Ordinary backend errors are logged and skipped. A user-facing error propagates and aborts the whole pass (other backends included). A member declared :data:~advexp.settings.REQUIRED has no default to reset to: once its storage entries are gone, the member is unset, and reading it raises an error, as before any load.

reset_settings() classmethod

Reset all declared settings to their defaults (no storage access).

A member declared :data:~advexp.settings.REQUIRED has no default to serve, so the pass clears its current value instead. This leaves the member in the never-loaded state, in which reading it raises an error: the state that a fresh declaration has before any load. The other members are reset in the normal way, whatever the position of the required member in the declaration order.

validate_settings() classmethod

Check every declared setting against all its constraints.

This is the strict manual check, in the same family as the load and save passes. In one pass, it checks the field constraints (with their when= conditions resolved against the current values), the cross-member rules (a comparison bound that references another member), and the __validate__ hook. Every violation of every member is collected into one :class:~advexp.settings_base.setting_validation_error.SettingValidationError, which carries the per-member report in its problems attribute. A CLI or a test can read this report as data. Nothing changes: values and storages stay exactly as they were. If everything passes, nothing is raised:

LocalSettings.validate_settings()

load_setting(setting_name) classmethod

Load one setting from its storage and report the fallback of the member.

setting_name is the member's name or a getter lambda that selects it: load_setting(lambda s: s.port).

The return value is the row of this member in the report that :meth:load_settings returns:

  • None, if the member got its value, or got its default in the normal way (the storage held nothing for it).
  • A :class:~advexp.settings.MemberLoadError (:class:~advexp.settings.SourceLoadError, :class:~advexp.settings.ConversionError, or :class:~advexp.settings.ConstraintViolationError), if a member with a default fell back to it. This is the same entry that the report of the class pass would carry.

A member declared :data:~advexp.settings.REQUIRED raises an error instead. There is no aggregate for a single member, so the caller sees the own error of the member (for example, a failed source, or a read of a never-loaded member). A user-facing storage condition is raised unwrapped, as in the class pass.

save_setting(setting_name) classmethod

Save the current value of one setting.

setting_name is the member's name or a getter lambda selecting it: save_setting(lambda s: s.port).

delete_setting(setting_name) classmethod

Delete one setting from its storage and reset it to its default.

setting_name is the member's name or a getter lambda that selects it: delete_setting(lambda s: s.port). A member declared :data:~advexp.settings.REQUIRED is left unset after the delete: it has no default to stand at, so reading it raises an error, as before any load.

reset_setting(setting_name) classmethod

Reset one setting to its default (no storage access).

setting_name is the member's name or a getter lambda that selects it: reset_setting(lambda s: s.port). A member declared :data:~advexp.settings.REQUIRED is cleared instead: it has no default to stand at, so reading it raises an error, as before any load.

contains_setting(setting_name) classmethod

Return whether the setting's storage holds a value.

setting_name is the member's name or a getter lambda selecting it: contains_setting(lambda s: s.port).

to_dict() classmethod

Return a snapshot of all declared settings, keyed by member name.

Values are deep-copied, so changing the snapshot never affects the class (and vice versa). Members that were never assigned or loaded report their effective default. Secure settings are included as they are, so treat the snapshot accordingly:

snapshot = LocalSettings.to_dict()

update_from_dict(data, *, ignore_unknown=False) classmethod

Set declared settings from a mapping keyed by member name.

Values are converted to the annotated types exactly as on load ("8080" for an int member becomes 8080). Then the whole update is checked against every declared constraint over the merged state (the new values together with the current ones): the own constraints of each member with its when= condition, the cross-member rules, and the __validate__ hook. If there is a violation, every finding is collected into one :class:SettingValidationError and nothing is applied. The values that are passed are deep-copied, so the class storage never shares mutable state with the mapping.

Keys that are not declared settings raise KeyError. The update is a mutator, and a mistyped name should fail loudly, instead of hiding in a partial update. For a dictionary from elsewhere (for example, the model_dump() of another model that has fields this class never declared), pass ignore_unknown=True instead: the unknown keys are skipped, with a debug record, and the declared ones are applied.

Storages are not written. Call :meth:save_settings to persist the values:

LocalSettings.update_from_dict({'airplane_mode': True})
LocalSettings.save_settings()

to_object() classmethod

Return a snapshot of all declared settings as a detached instance.

The returned instance is of the settings class itself, so its fields carry the declared names and annotations. If you rename a setting, every stale field access on a snapshot becomes a type-checker error (and an AttributeError at runtime). The instance holds its own deep-copied values: reading or writing them never affects the class, and vice versa. Members that were never assigned or loaded report their effective default. Secure settings are included as they are, so treat the snapshot accordingly. Apply the snapshot back with :meth:update_from_object:

snapshot = LocalSettings.to_object()

update_from_object(obj, *, ignore_unknown=False) classmethod

Set declared settings from the fields of an object.

The object is usually a snapshot from :meth:to_object. Any object with a plain attribute namespace also works (for example, SimpleNamespace(**to_dict()) or a dataclass instance), and the field names are the member names. Values are converted to the annotated types, and the whole update is checked over the merged state, exactly as in :meth:update_from_dict. The values are then deep-copied.

Field names that are not declared settings raise KeyError. ignore_unknown=True skips them in the same way, with a debug record; use it for an object from elsewhere whose namespace has fields that this class never declared. A failed conversion or check applies nothing.

Storages are not written. Call :meth:save_settings to persist the values:

LocalSettings.update_from_object(snapshot)
LocalSettings.save_settings()

get_plugin(plugin_class) classmethod

Return the plugin instance bound to this settings class (cached).

set_setting_metadata(setting_name, metadata_name, metadata_value) classmethod

Store metadata for a setting on this class, keyed by member name.

None is a value like any other; removing an entry is the job of :meth:delete_setting_metadata. The entry is stored in the own storage of this class, and the readers along the chain (this class and its subclasses) resolve it live. A setting reference that the class does not declare raises KeyError; every setting-metadata operation applies the same check. Values are deep-copied in both directions, so the stored metadata and the objects of the caller never share mutable state.

get_setting_metadata(setting_name, metadata_name, default=None) classmethod

Read one metadata entry of a setting; the nearest entry wins.

The ChainMap rule applies along the inheritance chain of the member (from this class down to the class that declared the member): the most derived entry that holds the key wins, whichever side declared or wrote it.

get_setting_all_metadata(setting_name) classmethod

Return every metadata entry of one setting, keyed by metadata name.

This is the snapshot form of :meth:get_setting_metadata, and the way to enumerate everything that the setting carries along its chain. For each key, the most derived entry wins. A runtime write adds keys on top of the inherited ones without removing them. A member that is redeclared in a subclass carries only the metadata of the redeclaration, because its chain ends at the subclass. The dict and its values are deep-copied. A setting with nothing stored anywhere in its chain returns an empty dict.

setting_name accepts a getter in place of the name string. A member that the class does not declare raises KeyError, the same check that every setting-metadata operation applies.

contains_setting_metadata(setting_name, metadata_name) classmethod

Return whether a metadata entry of the setting is stored under the name.

This is the unambiguous form of :meth:get_setting_metadata. get returns its default both for a missing entry and for a stored None, while contains tells the two cases apart. It checks the whole chain, with the same resolution that get applies. setting_name accepts a getter in place of the name string, like the other setting operations.

delete_setting_metadata(setting_name, metadata_name) classmethod

Remove the own entry of the class for a setting; a missing entry is not an error.

Only what this class itself declared or stored is removed. An entry of the declaring class or of another class in the chain stays visible, in the same way that deleting an attribute of a subclass makes the lookup fall back to the attribute of the base class. The method returns whether the class had an entry of its own to remove. An entry that was seeded by the metadata= declaration of the member is removed in the same way: the declaration seeds the storage only once, and the entry does not come back.

set_class_metadata(metadata_name, metadata_value) classmethod

Store class-level metadata on this class; None is a value like any other.

The entry is stored in the own storage of this class, and the readers along the inheritance chain (this class and its subclasses) resolve it live. The write reaches every subclass at once, while the base classes are never touched. Removing an entry is the job of :meth:delete_class_metadata. Values are deep-copied in both directions, so the stored metadata and the objects of the caller never share mutable state.

get_class_metadata(metadata_name, default=None) classmethod

Read a class-level metadata entry; the nearest entry wins.

The rule is the same as for class attribute lookup: the own storage of this class first, then the storages of the base classes. Declarations and runtime writes are resolved alike, and they are resolved live, so a runtime write into a base class is what a subclass reads.

get_class_all_metadata() classmethod

Return every class-level metadata entry, keyed by metadata name.

This is the snapshot form of :meth:get_class_metadata, and the way to enumerate everything that the class carries: its own entries and those of its chain, in one dict of deep-copied values. A class with nothing stored anywhere in its chain returns an empty dict.

contains_class_metadata(metadata_name) classmethod

Return whether a class-level metadata entry is stored under the name.

This is the unambiguous form of :meth:get_class_metadata. get returns its default both for a missing entry and for a stored None, while contains tells the two cases apart. It checks the whole inheritance chain, with the same resolution that get applies.

delete_class_metadata(metadata_name) classmethod

Remove the own class-level metadata entry of the class; a missing entry is not an error.

Only what this class itself declared or stored is removed. An entry of an ancestor stays visible, in the same way that deleting an attribute of a subclass makes the lookup fall back to the attribute of the base class. The method returns whether the class had an entry of its own to remove. An entry that was seeded by the declaration in the class body is removed in the same way: the declaration seeds the storage only once, and the entry does not come back.

delete_class_all_metadata() classmethod

Remove every own class-level metadata entry of the class.

Only the own storage of the class is cleared. The entries of the ancestors stay visible to the readers of the chain: clearing a subclass does not clear its base classes. The iteration order of a dynamic plugin and the entries that were seeded by the declaration in the class body are own entries of the class, and they are removed with the rest.

Library configuration

advexp.settings.LibraryConfiguration

Library-wide configuration (all state is class-level).

The registries of the family live here (attributes to serializers, plugin classes, and class-metadata validators), next to the parameter that an application touches: the log level of the whole family. The form of storage names is not part of it: a name pattern is the own vocabulary of the naming backend, and every backend family carries its own.

register_settings_plugin(plugin_class) classmethod

Register a plugin class and call its setup() hook, if there is one.

A plugin may define a setup() class method. It runs once, right at registration, and it is the place where a plugin usually registers its attribute-to-serializer pairs through :meth:register_settings_attribute. setup() is optional: a plugin may register its pieces inline at import time instead.

:class:~advexp.settings_base.settings_base_plugin.SettingsBasePlugin ships a setup(). It registers the declared attribute_class/serializer_class pair, so a plugin that derives from it usually declares the pair instead of writing its own setup(). Bundled plugins call this method when their module is imported, which is what makes importing a plugin module enable the plugin.

Registering a plugin that is already registered is a no-op: the setup() hook does not run a second time, so repeated imports or an explicit re-registration leave the state of the plugin untouched.

registered_plugin_classes() classmethod

Return the registered plugin classes, in registration order.

register_settings_attribute(attribute_class, serializer_factory) classmethod

Map an attribute class to the serializer class that serves its settings.

Registering the same pair again is a no-op: several plugins may register one shared attribute-to-serializer pair. Registering the same attribute class with a different serializer class raises :class:ValueError: two backends that claim one attribute are a conflict, not an override.

A dynamic-attribute pin is registered in the same way, as a slot of its own: the serializer that is bound to it serves only the dynamic entries, and it never touches the registration of the attribute class that the pin namespaces.

register_class_metadata_validator(metadata_name, validator) classmethod

Map a class metadata key to the validator of its values.

A plugin family that owns a metadata key (its options vocabulary) registers the validator when its module is imported. This is the same convention that the plugin registry follows, where the import enables the plugin.

The machinery then checks every stored value. A declaration in a class body makes the class statement fail, and a runtime call of :meth:~advexp.settings_base.context.SettingsContext.set_class_metadata raises an error before the value is stored. Keys that have no validator belong to the open channel alone: whatever the application stores there is never questioned.

Registering the same pair again is a no-op: a family module that is imported twice leaves the state untouched. Registering the same key with a different validator raises :class:ValueError: two families that claim one key are a conflict, not an override.

set_log_level(level) classmethod

Set the log level on the root of the family, which governs every channel.

The records of the machinery are carried by the channel of the distribution that drives them (advexp.settings_base, or the own channel of a distribution). All of these channels are under the advexp root, so one level that is set here covers them, whichever distribution is in use. A channel that has its own explicitly set level keeps it, because the logging module resolves the nearest level that is set.

logger() classmethod

Return the logging channel of the family, ready to be configured.

Every channel of the family (the own channel of a distribution, and the one of the foundation) lives under the root of the family. One object therefore receives every record that the machinery writes: you can set a level on it or add a handler with a formatter to it, all with standard logging. The name stays the own spelling of the library: the application configures the channel that it was handed, and never one that it looked up by name:

handler = logging.StreamHandler()
handler.setFormatter(
    logging.Formatter("%(levelname)s:%(name)s: %(message)s")
)
logger = LibraryConfiguration.logger()
logger.addHandler(handler)
logger.propagate = False  # the application's root stays quiet

The propagation parameter matters when the application also configures the root logger. logging.basicConfig() puts a handler there, so a channel that is configured here would duplicate its records. Turning the propagation off prevents this. An application that configures nothing but basicConfig needs none of this: the records flow to its root in the usual way.

Attribute parameters

The forms that a member's declaration can take. The plain parameter and the non-storage parameter come from the package root; the file families provide theirs in their plugin modules — the same modules that the examples import as js, ts, ys, env and dotenv.

advexp.settings.setting(*, name=None, default=REQUIRED, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Declare a member that the own source binding of the class resolves. This is the type-checker-friendly spelling of :class:Setting.

It is the rich form of the plain declaration: the member reads the one source that its class declares through options=, and the family of the binding builds its address. If default= is omitted (or is REQUIRED), the member is required: the source must provide its value, or the load aborts.

name= is the full literal address in the storage of the member: the environment prefix, the case, and the file key pattern never change it. aliases= are full literal addresses in the same way. A load tries the primary address first, then the aliases in their order.

The keyword parameters after aliases are the declared value constraints: gt/ge/lt/le/eq/ne, min_length/max_length, regex, validator, and when. They are applied to whatever the source provides, and they are the same constraints that every member factory of the family applies.

advexp.settings.Setting(*, name=None, default=REQUIRED, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

The attribute of a member that the own source binding of the class resolves.

The attribute names the setting, not the storage. A member declared through it has no backend of its own: it reads the one source that the class declares through options=. Both the plain declaration (token: str) and the rich one (:func:setting) produce this attribute. The family of the binding builds the address of the member by its own rules (the environment prefix and case, the JSON key pattern), so the member is addressed according to the storage that it actually reads. A declared name= is a full literal address in that storage.

advexp.settings.non_storage_setting(*, name=None, default=NO_DEFAULT, metadata=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Declare a member without a storage. This is the type-checker-friendly spelling.

This is the own declaration of the core: a member that no storage ever serves. Its value is the declared default or the last assignment, and the load pass resets it to the declared default. Save and delete skip it, while update_from_dict and update_from_object set it like any other member, and to_dict includes it. A plain function that returns Any is the spelling that mypy accepts; the factories of the individual backends (json_setting and the like) use the same form.

default= is required: a member that no storage ever serves must state its value, and the factory refuses a declaration without one.

The keyword parameters after metadata are the declared value constraints. gt/ge/lt/le/eq/ne compare the value, min_length and max_length bound len(value), regex is an unanchored pattern that a str value must match, and validator is a callable that receives the value and returns whether it passes. They apply wherever a value reaches the member: declared defaults, assignment, load, update_from_dict and update_from_object, and save.

A comparison parameter also accepts the attribute object of a member that was declared earlier in the same class body. This is a member reference (ge=min_port). It is compared with the current value of that member wherever the whole class is checked at once (class creation, load, update, save, :meth:~advexp.settings.Settings.validate_settings). when= takes only such a reference: while the referenced member is falsy, none of the constraints of this member apply. Rules that a comparison cannot state (for example, conditional requirements or "at least one of") belong to the __validate__ hook of the settings class.

advexp.settings.plugins.json_settings.json_setting(*, name=None, default=NO_DEFAULT, path=None, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Declare a JSON-file setting. This is the type-checker-friendly spelling of JSONSetting.

It builds exactly the attribute that the class builds, with the same keyword parameters. Both spellings exist because of typing. Assigning an attribute instance to an annotated member (port: int = JSONSetting(...)) is valid Python (the settings metaclass replaces the attribute with a value descriptor), but type checkers cannot see the replacement and report a type mismatch. They accept a plain function that returns Any. The Any return also leaves default= unchecked, deliberately: a default may be given in its storage form (default="4" for an int member), and it is converted at class creation, where a default that cannot be converted makes the class statement fail.

aliases= declares additional full storage keys. A load looks up the primary key first, then the aliases in their order, and the first key found wins. Every alias is placed under the same path=.

The keyword parameters after aliases are the declared value constraints (gt/ge/lt/le/eq/ne, min_length/max_length, regex, validator, when). They are the same constraints that :func:~advexp.settings.setting documents, and they are applied wherever a value reaches the member.

advexp.settings.plugins.json_settings.JSONSetting(*, name=None, default=NO_DEFAULT, path=None, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Setting attribute: the value is stored in the JSON settings file.

name= is the storage key and may contain any characters. Without it, the {module}.{class}.{member} pattern is used. The optional path= locates the key in the document tree as a JSON Pointer (RFC 6901): a leading / and slash-separated segments, where ~1 escapes / and ~0 escapes ~. For example:

  • path="/net/proxy" stores the setting inside {"net": {"proxy": {...}}}.
  • path="/mirrors/1" stores it inside the second element of the mirrors array.
  • path="/endpoints/api.internal" stores it under a key that contains a dot.

A segment that consists only of digits indexes an array if the node it steps into is an array, and names an object key otherwise. path="" and path=None both mean the document root.

advexp.settings.plugins.toml_settings.toml_setting(*, name=None, default=NO_DEFAULT, path=None, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Declare a TOML-file setting. This is the type-checker-friendly spelling of TOMLSetting.

It builds exactly the attribute that the class builds, with the same keyword parameters. Both spellings exist because of typing. Assigning an attribute instance to an annotated member (port: int = TOMLSetting(...)) is valid Python (the settings metaclass replaces the attribute with a value descriptor), but type checkers cannot see the replacement and report a type mismatch. They accept a plain function that returns Any. The Any return also leaves default= unchecked, deliberately: a default may be given in its storage form (default="4" for an int member), and it is converted at class creation, where a default that cannot be converted makes the class statement fail.

aliases= declares additional full storage addresses. A load looks up the primary address first, then the aliases in their order, and the first address found wins. Every alias is placed under the same path= and is nested by its own dots.

The keyword parameters after aliases are the declared value constraints (gt/ge/lt/le/eq/ne, min_length/max_length, regex, validator, when). They are the same constraints that :func:~advexp.settings.setting documents, and they are applied wherever a value reaches the member.

advexp.settings.plugins.toml_settings.TOMLSetting(*, name=None, default=NO_DEFAULT, path=None, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Setting attribute: the value is stored in the TOML settings file.

name= is the address of the setting in the document. Every dot in the name (the table separator of TOML) separates a table level, and the last part is the key. For example, name="net.host" stores the key inside the [net] table, and a name without a dot is a plain key. Without a name, the {module}.{class}.{member} pattern is used, and its dots nest in the same way. A literal key that contains a dot is reached through path=, because the segments of a pointer are never split.

The optional path= locates the key in the document tree as a JSON Pointer (RFC 6901): a leading / and slash-separated segments, where ~1 escapes / and ~0 escapes ~. For example:

  • path="/net/proxy" stores the setting inside [net.proxy].
  • path="/mirrors/1" stores it inside the second element of the mirrors array.
  • path="/endpoints/api.internal" stores it under a key that contains a dot.

A segment that consists only of digits indexes an array if the node it steps into is an array, and names a table key otherwise. path="" and path=None both mean the document root.

advexp.settings.plugins.yaml_settings.yaml_setting(*, name=None, default=NO_DEFAULT, path=None, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Declare a YAML-file setting. This is the type-checker-friendly spelling of YAMLSetting.

It builds exactly the attribute that the class builds, with the same keyword parameters. Both spellings exist because of typing. Assigning an attribute instance to an annotated member (port: int = YAMLSetting(...)) is valid Python (the settings metaclass replaces the attribute with a value descriptor), but type checkers cannot see the replacement and report a type mismatch. They accept a plain function that returns Any. The Any return also leaves default= unchecked, deliberately: a default may be given in its storage form (default="4" for an int member), and it is converted at class creation, where a default that cannot be converted makes the class statement fail.

aliases= declares additional full storage keys. A load looks up the primary key first, then the aliases in their order, and the first key found wins. Every alias is placed under the same path=.

The keyword parameters after aliases= are the declared value constraints (gt/ge/lt/le/eq/ne, min_length/max_length, regex, validator, when). They are the same constraints that :func:~advexp.settings.setting documents, and they are applied wherever a value reaches the member.

advexp.settings.plugins.yaml_settings.YAMLSetting(*, name=None, default=NO_DEFAULT, path=None, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Setting attribute: the value is stored in the YAML settings file.

name= is the storage key and may contain any characters. A dot in it stays inside the key: the name is flat and does not create a nesting level. Without name=, the {module}.{class}.{member} pattern is used. The optional path= locates the key in the document tree as a JSON Pointer (RFC 6901): a leading / and slash-separated segments, where ~1 escapes / and ~0 escapes ~. For example:

  • path="/net/proxy" stores the setting inside {"net": {"proxy": {...}}}.
  • path="/mirrors/1" stores it inside the second element of the mirrors sequence.
  • path="/endpoints/api.internal" stores it under a key that contains a dot.

A segment that consists only of digits indexes a sequence if the node it steps into is a sequence, and names a mapping key otherwise. path="" and path=None both mean the document root.

advexp.settings.plugins.environment.variables.environment_variable_setting(*, name=None, default=NO_DEFAULT, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Declare an environment-variable setting. This is the type-checker-friendly spelling of EnvironmentVariableSetting.

It is the same attribute with the same keyword parameters. Only the spelling differs, and the reason for it is typing. Assigning an attribute instance to an annotated member is valid Python (the settings metaclass replaces it with a value descriptor), but type checkers cannot see the replacement and report a type mismatch. They accept a plain function that returns Any, and the Any also prevents a default in storage form (default="8080" for an int member) from being flagged.

aliases= declares additional full variable names. A load looks up the primary name first, then the aliases in their order, and the first name found wins. This is how a rename is migrated: the old name keeps working next to the new one.

The keyword parameters after aliases are the declared value constraints (gt/ge/lt/le/eq/ne, min_length/max_length, regex, validator, when). They are the same constraints that :func:~advexp.settings.setting documents, and they are applied wherever a value reaches the member.

advexp.settings.plugins.environment.dotenv.dotenv_setting(*, name=None, default=NO_DEFAULT, metadata=None, aliases=None, gt=None, ge=None, lt=None, le=None, eq=None, ne=None, min_length=None, max_length=None, regex=None, validator=None, when=None)

Declare a dotenv-file setting. This is the type-checker-friendly spelling of DotenvSetting.

It is the same attribute with the same keyword parameters. Only the spelling differs, and the reason for it is typing. Assigning an attribute instance to an annotated member is valid Python (the settings metaclass replaces it with a value descriptor), but type checkers cannot see the replacement and report a type mismatch. They accept a plain function that returns Any, and the Any also prevents a default in storage form (default="8080" for an int member) from being flagged.

aliases= declares additional full variable names. A load looks up the primary name first, then the aliases in their order, and the first name found wins.

The keyword parameters after aliases are the declared value constraints (gt/ge/lt/le/eq/ne, min_length/max_length, regex, validator, when). They are the same constraints that :func:~advexp.settings.setting documents, and they are applied wherever a value reaches the member.

Source builders

The only way to bind a class to its storage. A builder is passed as the options= argument of the class statement. Use one builder per family; a class can use as many families as it has storages.

advexp.settings.plugins.json_settings.json_source(file=None, name_pattern=None, dynamic_file=None, parameters=None)

Bind a settings class to its JSON file storage, in the form used by this distribution.

Pass the result of the builder to the options= keyword of the class statement, either alone (for a class that keeps everything in one JSON file) or next to the builders of other families in one options= sequence:

class AppSettings(Settings, options=js.json_source(file="app-settings.json")):
    token: str

file names the file of the declared members of the class. A builder without file uses the module default of the backend. The name may contain template slots that are resolved through environment variables: json_source(file="settings-{APP_ENV}.json", parameters=app_env) reads the file of the environment that APP_ENV names. The parameters are declared once next to the binding and are shared by every templated path of the class.

name_pattern sets the pattern that builds the keys. If it is omitted, the full {module}.{class}.{member} pattern is used. The explicit name= of a member is the full address in the document and never passes through the pattern.

dynamic_file moves the dynamic entries of the class (the settings with names given at runtime) into a JSON file of their own. A class without it keeps them in the static file. The name may contain template slots in the same way as file, filled from the same parameters.

The binding is a metadata entry of the class itself. A subclass without its own json_source inherits the nearest one above it, and a subclass with its own json_source switches to it for itself and its subclasses. The base class keeps reading its own file. Two json_source builders in one options= make the class statement fail, as for every metadata name that is declared twice.

advexp.settings.plugins.toml_settings.toml_source(file=None, name_pattern=None, dynamic_file=None, parameters=None)

Bind a settings class to its TOML file storage, in the form used by this distribution.

Pass the result of the builder to the options= keyword of the class statement, either alone (for a class that keeps everything in one TOML file) or next to the builders of other families in one options= sequence:

class AppSettings(Settings, options=ts.toml_source(file="app-settings.toml")):
    token: str

file names the file of the declared members of the class. A builder without file uses the module default of the backend. The name may contain template slots that are resolved through environment variables: toml_source(file="settings-{APP_ENV}.toml", parameters=app_env) reads the file of the environment that APP_ENV names. The parameters are declared once next to the binding and are shared by every templated path of the class.

name_pattern sets the pattern that builds the keys. If it is omitted, the full {module}.{class}.{member} pattern is used. The explicit name= of a member is the full address in the document and never passes through the pattern.

dynamic_file moves the dynamic entries of the class (the settings with names given at runtime) into a TOML file of their own. A class without it keeps them in the static file. The name may contain template slots in the same way as file, filled from the same parameters.

The binding is a metadata entry of the class itself. A subclass without its own toml_source inherits the nearest one above it, and a subclass with its own toml_source switches to it for itself and its subclasses. The base class keeps reading its own file. Two toml_source builders in one options= make the class statement fail, as for every metadata name that is declared twice.

advexp.settings.plugins.yaml_settings.yaml_source(file=None, name_pattern=None, dynamic_file=None, parameters=None)

Bind a settings class to its YAML file storage, in the form used by this distribution.

Pass the result of the builder to the options= keyword of the class statement, either alone (for a class that keeps everything in one YAML file) or next to the builders of other families in one options= sequence:

class AppSettings(Settings, options=ys.yaml_source(file="app-settings.yaml")):
    token: str

file names the file of the declared members of the class. The extension is not checked: any path works. A builder without file uses the module default of the backend. The name may contain template slots that are resolved through environment variables: yaml_source(file="settings-{APP_ENV}.yaml", parameters=app_env) reads the file of the environment that APP_ENV names. The parameters are declared once next to the binding and are shared by every templated path of the class.

name_pattern sets the pattern that builds the keys. If it is omitted, the full {module}.{class}.{member} pattern is used. The explicit name= of a member is the full address in the document and never passes through the pattern.

dynamic_file moves the dynamic entries of the class (the settings with names given at runtime) into a YAML file of their own. A class without it keeps them in the static file. The name may contain template slots in the same way as file, filled from the same parameters.

The binding is a metadata entry of the class itself. A subclass without its own yaml_source inherits the nearest one above it, and a subclass with its own yaml_source switches to it for itself and its subclasses. The base class keeps reading its own file. Two yaml_source builders in one options= make the class statement fail, as for every metadata name that is declared twice.

advexp.settings.plugins.environment.variables.env_source(name_prefix=None, name_case=None, nested_delimiter=None)

Bind a settings class to the process environment, in the form used by this distribution.

Pass the result of the builder to the options= keyword of the class statement, either alone (for a class that reads nothing else) or next to the builders of other families in one options= sequence:

class AppSettings(Settings, options=env.env_source(
    name_prefix="APP_", name_case=env.NameCase.UPPER,
)):
    mode: str

The naming parameters name_prefix, name_case, and nested_delimiter declare the naming dictionary of the family for this class. A class has only one seat for these rules, and the process environment and a .env file share it, because they are one variable namespace. Declare the dictionary either on this builder or on dotenv_source, but only once per class: two declarations make the class statement fail, as for every metadata name that is declared twice. A class that declares no dictionary reads member names as written, with no prefix.

The binding is a metadata entry of the class itself. A subclass without its own environment binding inherits the nearest one above it, and a subclass with its own binding switches to it for itself and its subclasses. The base class keeps reading its own variables.

advexp.settings.plugins.environment.variables.NameCase

The letter-case regime of the member-name part of an environment name.

This is the spelling of the name_case option in code: name_case=NameCase.UPPER.

  • UPPER and LOWER change the case of the member name in every built name.
  • ANY leaves the built name as declared, and makes its lookup case-insensitive instead.

It is a str enum, because its members also serve as the values of the stored data form. The class metadata keeps the plain "upper"/"lower"/"any" strings, and the raw metadata channel (a hand-written entry, or a runtime set_class_metadata call) accepts both spellings. A member compares equal to its string.

advexp.settings.plugins.environment.dotenv.dotenv_source(file=None, name_prefix=None, name_case=None, nested_delimiter=None, parameters=None)

Bind a settings class to a .env file, in the form used by this distribution.

Pass the result of the builder to the options= keyword of the class statement, either alone (for a class that reads nothing else) or next to the builders of other families in one options= sequence:

class AppSettings(Settings, options=dotenv_source(
    file=".env", name_prefix="APP_", name_case=NameCase.UPPER,
)):
    mode: str

file names the file that the backend reads and writes. A builder without file uses the module default of the backend. An absent file behaves as an empty one: required members are reported, and defaults stand in. The name may contain template slots that are resolved through environment variables: dotenv_source(file=".env-{APP_ENV}", parameters=app_env) reads the file of the environment that APP_ENV names.

The naming parameters name_prefix, name_case, and nested_delimiter declare the naming dictionary of the family for this class. A class has only one seat for these rules, and the process environment and this file share it. Declare the rules either on this builder or on env_source, but only once per class: two declarations make the class statement fail, as for every metadata name that is declared twice. A class that declares no dictionary reads member names as written, with no prefix.

The binding is a metadata entry of the class itself. A subclass without its own dotenv_source inherits the nearest one above it, and a subclass with its own dotenv_source switches to it for itself and its subclasses. The base class keeps reading its own file.

advexp.settings.plugins.mapping_source.mapping_source(mapping=None)

Bind a settings class to its dictionary storage.

Pass the result of the builder to the options= keyword of the class statement:

class AppSettings(Settings, options=mapping_source(BASE_DEFAULTS)):
    token: str

mapping seeds the storage. The dictionary is copied at the call, so a later change to the object of the caller never reaches the storage, and every value that a load finds is served as a fresh copy in the same way. If the builder is called without a dictionary, it binds an empty storage. The application fills it at runtime through mapping_source_for(AppSettings).set({...}), and a load that runs before any set passes over the storage as a miss.

The storage uses member names only: an explicit name= or aliases= of a member changes nothing here, and a key that names no member is dropped, with a debug record. A save writes only the member keys and leaves the rest of the dictionary as it is. A delete removes the key of its member in the same way.

The storage is the only source binding of the class. If a class declares it next to another source builder, the class statement fails. The own seats of a family (json_file(...), or a factory member of a family) can be used next to it without restrictions.

advexp.settings.plugins.mapping_source.mapping_source_for(cls)

The mapping storage of cls: the link of the binding, with set and get.

Bind the storage with mapping_source(...) in the class statement, and reach it from the class:

mapping_source_for(AppSettings).set({"token": "dev-token"})
AppSettings.load_settings()

The resolver reads the nearest binding. A subclass without its own mapping_source shares the storage of its base class: one dictionary, one set, one get. The resolver raises an error that names the class if the class was not built by the family, or if the class has no mapping binding.

The link of the binding: the dictionary of the mapping storage, with the two handles of the resolver on it.

The link carries the serializer of the storage and its dictionary, which is the seat that the whole family reads. set(mapping) and get() are the channel of the application. The loads and saves of the class are the other channel, and both channels work on the same dictionary.

set(mapping)

Replace the dictionary of the storage with mapping. The link stores a copy, not a view.

The replacement is complete: keys that the new dictionary does not contain are removed, whatever the previous dictionary held. This includes the keys that the saves of the class wrote. The save channel and this channel are two separate channels, and a save never brings back what a set has removed. The dictionary is deep-copied on the way in, and get returns a copy, so the object of the caller and the state of the storage share nothing.

get()

Return the dictionary of the storage as it stands now: a deep copy, so changes that the caller makes never reach the storage.

The dictionary holds the last set and the saves of the class together, keyed by member name. A foreign key that the application stored keeps its place among them.

File binders

advexp.settings.plugins.json_settings.json_file(path)

Name the JSON file of a settings class, in the form used by this distribution.

The file seat is the file of the class for its json_setting members, when the builder of another family holds the binding. The seat and its validation are part of the shared vocabulary of the backend; see :func:advexp.settings_base.json_file.json_file. The stamp makes the class statement of a class of another distribution reject the entry.

advexp.settings.plugins.toml_settings.toml_file(path)

Name the TOML file of a settings class, in the form used by this distribution.

The file seat is the file of the class for its toml_setting members, when the builder of another family holds the binding. The seat and its validation are part of the shared vocabulary of the backend; see :func:advexp.settings_base.toml_file.toml_file. The stamp makes the class statement of a class of another distribution reject the entry.

advexp.settings.plugins.yaml_settings.yaml_file(path)

Name the YAML file of a settings class, in the form used by this distribution.

The file seat is the file of the class for its yaml_setting members, when the builder of another family holds the binding. The seat and its validation are part of the shared vocabulary of the backend; see :func:advexp.settings_base.yaml_file.yaml_file. The stamp makes the class statement of a class of another distribution reject the entry.

advexp.settings.plugins.environment.dotenv.dotenv_file(path)

Name the .env file of a settings class, in the form used by this distribution.

The file seat is the file of the class for its dotenv_setting members, when the builder of another family holds the binding. The seat and its validation are part of the shared vocabulary of the family; see :func:advexp.settings_base.dotenv_file.dotenv_file. The stamp makes the class statement of a class of another distribution reject the entry.

Path parameters

advexp.settings.env_parameter(variable, choices, default=None, slot=None)

Declare one path parameter that is read from an environment variable.

Pass the result to the parameters= of a builder of a source that carries a file, either alone or in a tuple:

app_env = env_parameter("APP_ENV", choices=("dev", "test", "prod"))

class AppConfig(Configuration, options=Sources(
    json_source(file="app.json"),
    json_source(file="settings-{APP_ENV}.json", parameters=app_env),
)):

variable names the environment variable, and the template spells the slot by that name: settings-{APP_ENV}.json.

slot= renames the slot if the template spells another name. The value still comes from variable, and the template spells the renamed slot, so slot="env" reads APP_ENV into settings-{env}.json. This is useful for a template that dictates its own slot names (a file shared with another system, or a path that reads better with a short slot). The name must be a valid identifier: otherwise, the braces would stay as literal text instead of becoming a slot.

choices is the obligatory whitelist of which the value of the variable must be one. It is checked before any file is touched.

default is the value to which an unset variable resolves. It must be one of the choices. Without it, an unset variable breaks the link at load time.

Module file defaults

advexp.settings.for_this_module(value)

Declare the value of a parameter for the module in which this call is written.

Write the call in the assignment itself, at the top level of the module:

holder.file_path = for_this_module(Path("app-settings.json"))

The function names the calling module at the call, and the metaclass of the holder consumes the marker at the moment when the assignment executes, during the import of the module. If you wrap the call in a helper of another module, it would name that other module instead: the declaration belongs to the assignment that it addresses.

The call is typed as Any on purpose: the marker can be assigned to every parameter that a holder declares, whatever the own type of the parameter is, and the runtime validates the value at the line of the assignment.

advexp.settings.declared_module_defaults()

Return a snapshot of every module declaration: (holder, parameter, module) keys over the declared values.

This is the reader for tools that collect the declarations without running the application. Importing the settings modules fills the registry, because a declaration at the module level executes at import, and one call hands everything to the collector. The snapshot is a deep copy: changing it changes nothing in the registry.

advexp.settings.plugins.json_settings.JSONSettingsConfiguration

JSON backend configuration (class-level state).

file_path = None class-attribute instance-attribute

Default settings file location (str or os.PathLike).

It applies to settings classes without a json_file(...) binding. When neither is set, the first storage access raises.

A settings module can keep its file to itself — declare it at the module's top level, with for_this_module from the package root:

JSONSettingsConfiguration.file_path = for_this_module(
    Path("app-settings.json")
)

The declaration holds for the classes of the module it is written in and wins over this attribute; every other module keeps reading the attribute, which stays the process-wide channel — set it once at startup for the whole application. The declaration is an ordinary module-level statement: it takes effect during the module's import, so a tool that imports the settings modules collects it without running the application.

advexp.settings.plugins.toml_settings.TOMLSettingsConfiguration

TOML backend configuration (class-level state).

file_path = None class-attribute instance-attribute

Default settings file location (str or os.PathLike).

It applies to settings classes without a toml_file(...) binding. When neither is set, the first storage access raises.

A settings module can keep its file to itself — declare it at the module's top level, with for_this_module from the package root:

TOMLSettingsConfiguration.file_path = for_this_module(
    Path("app-settings.toml")
)

The declaration holds for the classes of the module it is written in and wins over this attribute; every other module keeps reading the attribute, which stays the process-wide channel — set it once at startup for the whole application. The declaration is an ordinary module-level statement: it takes effect during the module's import, so a tool that imports the settings modules collects it without running the application.

advexp.settings.plugins.yaml_settings.YAMLSettingsConfiguration

YAML backend configuration (class-level state).

file_path = None class-attribute instance-attribute

Default settings file location (str or os.PathLike).

It applies to settings classes without a yaml_file(...) binding. When neither is set, the first storage access raises.

A settings module can keep its file to itself — declare it at the module's top level, with for_this_module from the package root:

YAMLSettingsConfiguration.file_path = for_this_module(
    Path("app-settings.yaml")
)

The declaration holds for the classes of the module it is written in and wins over this attribute; every other module keeps reading the attribute, which stays the process-wide channel — set it once at startup for the whole application. The declaration is an ordinary module-level statement: it takes effect during the module's import, so a tool that imports the settings modules collects it without running the application.

advexp.settings.plugins.environment.dotenv.DotenvConfiguration

Dotenv backend configuration (class-level state).

file_path = '.env' class-attribute instance-attribute

Default file location (str or os.PathLike).

A relative path resolves against the working directory of the running process. It applies to settings classes without a dotenv_file(...) binding; an absent file behaves as an empty one — the declared defaults stand in. The default is the ecosystem's conventional file, so the backend works with no configuration at all.

A settings module can keep its file to itself — declare it at the module's top level, with for_this_module from the package root:

DotenvConfiguration.file_path = for_this_module(Path("app.env"))

The declaration holds for the classes of the module it is written in and wins over this attribute; every other module keeps reading the attribute — the process-wide default above.

Dynamic settings

The plugins that store entries whose names are defined at runtime. The plugin object holds the section parameters and the runtime file_path; the configuration holder holds the module-level file and the name pattern.

advexp.settings.plugins.json_dynamic_settings.JSONDynamicSettingsPlugin()

Plugin object for dynamic settings in the JSON settings file.

The namespaced pin names the backend. Dynamic entries use the same serializer and the same file as the JSONSetting members of the class, so one document holds the static and the runtime settings of the class side by side: the static entries under their full names, and the dynamic ones in the dedicated section of the document (see :attr:DocumentDynamicSettingsPlugin.dynamic_settings_section_name and :attr:DocumentDynamicSettingsPlugin.dynamic_settings_section_path, inherited here).

The shared file is the default, not a rule. The dynamic_file= parameter of the json_source(...) binding of the class (class metadata; the nearest entry along the class chain wins) or the module-wide JSONDynamicSettingsConfiguration.file_path moves the dynamic entries to a file of their own, separate from the static one.

The pin is the own registry slot of the dynamic side: the binding ties it to JSONSettingsSerializer here and never touches the registration of JSONSetting by the static plugin.

file_path property writable

The own file for the dynamic entries of this class; None when the static file governs.

Reading resolves along the class chain: the own binding of the class, or otherwise the binding of the nearest ancestor. Assigning re-binds the class itself: the value is validated and stored as an own metadata entry of the class. The declarative json_source(dynamic_file=...) binding and an assignment here are therefore the same channel. The binding moves the dynamic entries of this class to the named file, separate from the static side, and it wins over JSONDynamicSettingsConfiguration.file_path.

None means that there is no binding anywhere. In that case, the module default applies. If the module default is also unset, the dynamic entries go into the static file. delete_class_metadata removes the binding again.

advexp.settings.plugins.json_dynamic_settings.JSONDynamicSettingsConfiguration

Dynamic JSON settings configuration (class-level state).

file_path = None class-attribute instance-attribute

Own file for the dynamic entries (str or os.PathLike).

When set, every dynamic collection without a binding of its own goes to this file, apart from the file of the declared (static) settings; the document inside is shaped by the section parameters of :class:JSONDynamicSettingsPlugin as usual.

A settings module can declare the dynamic entries' own file for itself alone — at the module's top level, with for_this_module from the package root:

JSONDynamicSettingsConfiguration.file_path = for_this_module(
    Path("dynamic-settings.json")
)

The declaration holds for the classes of the module it is written in and wins over this attribute; modules without one keep reading the attribute. When both are unset (the default), the dynamic entries share the static file: the class's own binding, the static module declaration, or JSONSettingsConfiguration.file_path.

name_pattern = DEFAULT_SETTINGS_NAME_PATTERN class-attribute instance-attribute

The name pattern of the dynamic entries; the default is the full pattern {module}.{class}.{name}.

The pattern shapes both visible names of an entry: the class object's key in the document's dynamic section (the part in front of the entry's own name) and, in the flat layout, the D-prefixed full name at the document root. The plain names the collection works with never change — the pattern spells them into the document. It may hold the {NamespaceName} (module), {ClassName}, {FieldName} and {Delimiter} slots, and it must hold {FieldName} and a class part: without {FieldName} the entries would collide, and without a class part the section layout has nothing to group (and parse back) the entries by — both are refused at the first pass that needs a name. The static members' keys are not governed here: they take their form from the static side's own naming options (the json_options family entry), and a backend without them keeps the default full pattern.

Changing the pattern is a migration of every file already written: the class object's key in the section and the flat full names change with it, and the entries stored under the old spelling are no longer found. That is the declaring side's own choice — set the pattern once, before the first stored file, or move the entries by hand when it changes.

advexp.settings.plugins.toml_dynamic_settings.TOMLDynamicSettingsPlugin()

Plugin object for dynamic settings in the TOML settings file.

The namespaced pin names the backend. Dynamic entries use the same serializer and the same file as the TOMLSetting members of the class, so one document holds the static and the runtime settings of the class side by side: the static entries under their full names, and the dynamic ones in the dedicated section of the document (see :attr:DocumentDynamicSettingsPlugin.dynamic_settings_section_name and :attr:DocumentDynamicSettingsPlugin.dynamic_settings_section_path, inherited here).

The shared file is the default, not a rule. The toml_source(dynamic_file=...) binding of the class (class metadata; the nearest entry along the class chain wins) or the module-wide TOMLDynamicSettingsConfiguration.file_path moves the dynamic entries to a file of their own, separate from the static one.

The pin is the own registry slot of the dynamic side: the binding ties it to TOMLSettingsSerializer here and never touches the registration of TOMLSetting by the static plugin.

file_path property writable

The own file for the dynamic entries of this class; None when the static file governs.

Reading resolves along the class chain: the own binding of the class, or otherwise the binding of the nearest ancestor. Assigning re-binds the class itself: the value is validated and stored as an own metadata entry of the class. The declarative toml_source(dynamic_file=...) binding and an assignment here are therefore the same channel. The binding moves the dynamic entries of this class to the named file, separate from the static side, and it wins over TOMLDynamicSettingsConfiguration.file_path.

None means that there is no binding anywhere. In that case, the module default applies. If the module default is also unset, the dynamic entries go into the static file. delete_class_metadata removes the binding again.

advexp.settings.plugins.toml_dynamic_settings.TOMLDynamicSettingsConfiguration

Dynamic TOML settings configuration (class-level state).

file_path = None class-attribute instance-attribute

Own file for the dynamic entries (str or os.PathLike).

When set, every dynamic collection without a binding of its own goes to this file, apart from the file of the declared (static) settings; the document inside is shaped by the section parameters of :class:TOMLDynamicSettingsPlugin as usual.

A settings module can declare the dynamic entries' own file for itself alone — at the module's top level, with for_this_module from the package root:

TOMLDynamicSettingsConfiguration.file_path = for_this_module(
    Path("dynamic-settings.toml")
)

The declaration holds for the classes of the module it is written in and wins over this attribute; modules without one keep reading the attribute. When both are unset (the default), the dynamic entries share the static file: the class's own binding, the static module declaration, or TOMLSettingsConfiguration.file_path.

name_pattern = DEFAULT_SETTINGS_NAME_PATTERN class-attribute instance-attribute

The name pattern of the dynamic entries; the default is the full pattern {module}.{class}.{name}.

The pattern shapes both visible names of an entry: the class object's key in the document's dynamic section (the part in front of the entry's own name) and, in the flat layout, the D-prefixed full name at the document root. The plain names the collection works with never change — the pattern spells them into the document. It may hold the {NamespaceName} (module), {ClassName}, {FieldName} and {Delimiter} slots, and it must hold {FieldName} and a class part: without {FieldName} the entries would collide, and without a class part the section layout has nothing to group (and parse back) the entries by — both are refused at the first pass that needs a name. The static members' keys are not governed here: they take their form from the static side's own naming options (the toml_options family entry), and a backend without them keeps the default full pattern.

Changing the pattern is a migration of every file already written: the class object's key in the section and the flat full names change with it, and the entries stored under the old spelling are no longer found. That is the declaring side's own choice — set the pattern once, before the first stored file, or move the entries by hand when it changes.

advexp.settings.plugins.yaml_dynamic_settings.YAMLDynamicSettingsPlugin()

Plugin object for dynamic settings in the YAML settings file.

The namespaced pin names the backend. Dynamic entries use the same serializer and the same file as the YAMLSetting members of the class, so one document holds the static and the runtime settings of the class side by side: the static entries under their full names, and the dynamic ones in the dedicated section of the document (see :attr:DocumentDynamicSettingsPlugin.dynamic_settings_section_name and :attr:DocumentDynamicSettingsPlugin.dynamic_settings_section_path, inherited here).

The shared file is the default, not a rule. The yaml_source(dynamic_file=...) binding of the class (class metadata; the nearest entry along the class chain wins) or the module-wide YAMLDynamicSettingsConfiguration.file_path moves the dynamic entries to a file of their own, separate from the static one.

The pin is the own registry slot of the dynamic side: the binding ties it to YAMLSettingsSerializer here and never touches the registration of YAMLSetting by the static plugin.

file_path property writable

The own file for the dynamic entries of this class; None when the static file governs.

Reading resolves along the class chain: the own binding of the class, or otherwise the binding of the nearest ancestor. Assigning re-binds the class itself: the value is validated and stored as an own metadata entry of the class. The declarative yaml_source(dynamic_file=...) binding and an assignment here are therefore the same channel. The binding moves the dynamic entries of this class to the named file, separate from the static side, and it wins over YAMLDynamicSettingsConfiguration.file_path.

None means that there is no binding anywhere. In that case, the module default applies. If the module default is also unset, the dynamic entries go into the static file. delete_class_metadata removes the binding again.

advexp.settings.plugins.yaml_dynamic_settings.YAMLDynamicSettingsConfiguration

Dynamic YAML settings configuration (class-level state).

file_path = None class-attribute instance-attribute

Own file for the dynamic entries (str or os.PathLike).

When set, every dynamic collection without a binding of its own goes to this file, apart from the file of the declared (static) settings; the document inside is shaped by the section parameters of :class:YAMLDynamicSettingsPlugin as usual.

A settings module can declare the dynamic entries' own file for itself alone — at the module's top level, with for_this_module from the package root:

YAMLDynamicSettingsConfiguration.file_path = for_this_module(
    Path("dynamic-settings.yaml")
)

The declaration holds for the classes of the module it is written in and wins over this attribute; modules without one keep reading the attribute. When both are unset (the default), the dynamic entries share the static file: the class's own binding, the static module declaration, or YAMLSettingsConfiguration.file_path.

name_pattern = DEFAULT_SETTINGS_NAME_PATTERN class-attribute instance-attribute

The name pattern of the dynamic entries; the default is the full pattern {module}.{class}.{name}.

The pattern shapes both visible names of an entry: the class object's key in the document's dynamic section (the part in front of the entry's own name) and, in the flat layout, the D-prefixed full name at the document root. The plain names the collection works with never change — the pattern spells them into the document. It may hold the {NamespaceName} (module), {ClassName}, {FieldName} and {Delimiter} slots, and it must hold {FieldName} and a class part: without {FieldName} the entries would collide, and without a class part the section layout has nothing to group (and parse back) the entries by — both are refused at the first pass that needs a name. The static members' keys are not governed here: they take their form from the static side's own naming options (the yaml_options family entry), and a backend without them keeps the default full pattern.

Changing the pattern is a migration of every file already written: the class object's key in the section and the flat full names change with it, and the entries stored under the old spelling are no longer found. That is the declaring side's own choice — set the pattern once, before the first stored file, or move the entries by hand when it changes.

Errors and sentinels

advexp.settings.SettingValidationError(message, problems=None)

A value violates the constraints that are declared on a setting member.

The error is raised wherever a value meets the declared constraints: assignment to a setting, update_from_dict/update_from_object, a save of a value that no longer satisfies them, and the manual validate_settings(). A load keeps its own error policy: it falls back to the default with a logged warning, like a value that does not convert.

A check over the whole settings object (an update, a save, or the manual validation) collects every violation of every member at once. Such an error carries the findings as problems: a dict of member name to the list of its problem descriptions. Tools can read the data instead of parsing the message, while the message itself lists the same findings for the reader of the log.

Subclassing :class:ValueError keeps a plain except ValueError working. In that case, a violation cannot be distinguished from a failed conversion, while the precise class can still be caught on its own.

advexp.settings.AggregateLoadError(errors, fallbacks=(), misses=(), channel=None)

The error that a failed load raises.

The pass walks every member before aborting, so one run reports every failure at once. The error has three groups of entries:

  • errors holds the individual exceptions that abort the pass, in declaration order: a required member that got no usable value, or a cross-member violation that no reset to the declared defaults could settle.
  • fallbacks holds the errors of the optional members whose sources provided a value that the members could not take (an unconvertible or invalid value). These members stand at their defaults.
  • misses holds the optional members whose sources provided no value at all. These members also stand at their defaults.

These are the structural marks that tell the three groups apart. The message carries each entry as a line. It opens with the channel of the distribution that ran the pass (channel, the name under which its records are logged), spelled as <channel> error: the load failed for ..., so an abort that the application prints names its package even away from the log.

Every load abort is this class, however many members failed: the type of the exception never depends on the number of failures. A class in which all members are optional aborts only on a violation that the resets cannot settle. Otherwise, its load report is the tuple that the loading method returns, and the misses stay out of it (a miss is the normal way in which a default gets used). The misses are attached only to an abort, where the reader of the failure wants to see the whole class at once.

The class is a subclass of :class:~advexp.settings_base.member_load_error.MemberLoadError, so a family-wide except MemberLoadError (or except ValueError) handler keeps catching load aborts.

advexp.settings.MemberLoadError(message, member_name=None)

A member could not get a valid value from its source.

This is the parent of the load-time reasons: the source provided no value, a value that cannot be converted, a constraint violation, or a failed source. It is also the parent of the aggregate abort of the load pass.

Where the reason is unknown, the error is raised directly. For example, reading a member that was never loaded serves its default, and a member that is declared :data:~advexp.settings_base.required.REQUIRED has none to serve.

The error names a problem, not a policy: the declaration of a member decides the channel. The load error of a required member is an entry of the load abort. The load error of an optional member is an entry of the load report that the loading method returns, and the member itself stands at its default. Subclassing :class:ValueError keeps a plain except ValueError working: the value errors of the family are all subclasses of ValueError.

member_name carries the member that the error is about, when the one who raises the error knows it. The required-read error of the member descriptor always knows it, so a handler can report the finding on that member without parsing the message. Errors that are raised without it leave it None.

advexp.settings.MissingValueError(message, member_name=None)

The load error of a member for which the source provided no value.

For a required member, this error goes into the errors of the load abort. An optional member treats a miss as the normal way of serving its default. Its miss error is attached only to an abort, in the misses of the aggregate, and it stays out of the report of a successful run.

The error is an entry of the load abort (:class:~advexp.settings_base.aggregate_load_error.AggregateLoadError). The message names the member, its storage name, and the class, so the reader knows exactly what to provide. The storage name is the own spelling of the source when it offers one (for example, the name of an environment variable), and otherwise it is the storage name that the pattern built.

advexp.settings.ConversionError(message, member_name=None)

The value of a member did not convert to the annotated type.

The source had a value for the member, but the value cannot take the annotated shape of the member. The error names the member, its setting, and the class, and it shows the value and the type to which the value would not convert (the value is masked when the member holds a secret). For a required member, it is an entry of the load abort (:class:~advexp.settings_base.aggregate_load_error.AggregateLoadError). For an optional member, it is an entry of the load report that the loading method returns, and the member stands at its default.

advexp.settings.ConstraintViolationError(message, member_name=None)

The value of a member violates the constraints that are declared on it.

The value was converted, but the own declared bounds of the member, or a rule across members, reject it. The error names the member, its setting, and the class, and it lists every violated constraint. For a required member, it is an entry of the load abort (:class:~advexp.settings_base.aggregate_load_error.AggregateLoadError). For an optional member, it is an entry of the load report that the loading method returns, and the member stands at its default.

advexp.settings.SourceLoadError(message, member_name=None)

The source of a member raised an error while loading its value.

The message names the member, its setting, and the class, and it carries the own error of the source (which also stays chained as the cause).

The handling depends on the kind of error and on the member:

  • A source that reports its problems through the user-facing channel (a corrupt or an unreadable file) is handled in the same way on a required member, with its exception carried in the message. On an optional member, it keeps propagating as its own exception, because a served default must not hide it.
  • A plain error of the source (whatever the backend raised without wrapping it) is an entry of the load report that the loading method returns for an optional member, next to the log record that the pass keeps.

advexp.settings.PluginNotEnabledError

A setting attribute has no registered serializer (plugin not imported).

advexp.settings.REQUIRED = _Required() module-attribute

The default that makes a member required: its value must come from a source.

Declared as the default (default=REQUIRED). Every boundary that would serve the default — a load that found nothing usable in the source, a read of a never-loaded member — raises :class:~advexp.settings_base.member_load_error.MemberLoadError instead: the load-time reasons are entries of the load pass's aggregate abort. The family-wide spelling of an optional member is :data:~advexp.settings_base.no_default.NO_DEFAULT: None stands in, and its load problems land in the report the loading method returns.

Secret wrappers

advexp.settings.SecretStr(value)

A string whose repr and str return the mask, not the value.

This is the annotated type of a member that carries a secret. Loading wraps the string of the source into it. A casual print, a log record, or an error message therefore shows '**********'. The only way to get the value is :meth:get_secret_value, which is an explicit step that is easy to grep for.

The constraint checks look at the inner string, while their messages keep the mask. The storage form is the plain string: the save path unwraps the value before the serializers see it. The masking is carried by the wrapper: a plain string that is assigned to the member keeps the semantics of a plain string.

get_secret_value()

Return the wrapped value. This call is the explicit step to take, and it is easy to grep for.

advexp.settings.SecretBytes(value)

Bytes whose repr and str return the mask, not the value.

This is the annotated type of a member that carries a secret. Loading wraps the payload of the source into it (a string is encoded as UTF-8, as the bytes conversion does). A casual print, a log record, or an error message therefore shows '**********'. The only way to get the value is :meth:get_secret_value, which is an explicit step that is easy to grep for.

The constraint checks look at the inner payload, while their messages keep the mask. The storage form is the plain bytes: the save path unwraps the value before the serializers see it. The masking is carried by the wrapper: plain bytes that are assigned to the member keep the semantics of plain bytes.

get_secret_value()

Return the wrapped value. This call is the explicit step to take, and it is easy to grep for.