Module xcon.config

Main config module. Key pieces such as Config and config are imported directly into "xcon".

Very quick example, this will grab SOME_CONFIG_ATTR for you:

>>> from xcon.config import config
>>> config.SOME_CONFIG_ATTR

TODO

Link to the readme.md docs

Expand source code
"""
Main config module. Key pieces such as `xcon.config.Config` and
`xcon.config.config` are imported directly into "xcon".

Very quick example, this will grab `SOME_CONFIG_ATTR` for you:

>>> from xcon.config import config
>>> config.SOME_CONFIG_ATTR

.. todo:: Link to the readme.md docs

"""
import os
from copy import copy
from dataclasses import dataclass, field
from inspect import isclass
from typing import (
    Dict, List, Union, Optional, Tuple, Iterable, Type, Any, Callable, TypeVar
)

# Note: pdoc3 can't resolve type-hints inside of method parameters with this enabled.
#   disabling it. (leaving it here for future reference for others)
# from __future__ import annotations
from xinject import XContext, Dependency
from xsettings import Settings, SettingsField
from xsettings.retreivers import SettingsRetrieverProtocol
from xsentinels import Default
from .types import OrderedDefaultSet, OrderedSet
from xsentinels.default import DefaultType
from xbool import bool_value
from xloop import xloop

from logging import getLogger

from .directory import Directory, DirectoryItem, DirectoryListing, DirectoryOrPath, DirectoryChain
from .exceptions import ConfigError
from .provider import Provider, ProviderChain, ProviderCacher, InternalLocalProviderCache
from .providers import EnvironmentalProvider
from .providers.dynamo import DynamoCacher

from xcon.conf import xcon_settings

xlog = getLogger(__name__)

T = TypeVar('T')


@dataclass(frozen=True, eq=False)
class _ParentCursor:
    parent: "Config"
    index: int
    chain: "_ParentChain"

    def next_cursor(self) -> Optional["_ParentCursor"]:
        chain = self.chain
        next_index = self.index + 1
        parents = chain.parents
        if next_index >= len(parents):
            return None
        next_config = parents[next_index]
        return _ParentCursor(parent=next_config, index=next_index, chain=chain)


@dataclass(frozen=True, eq=True)
class _ParentChain:
    parents: Tuple["Config"] = field(default_factory=list)

    def __post_init__(self):
        parents = self.parents
        if not isinstance(parents, tuple):
            # Convert to a tuple
            object.__setattr__(self, 'parents', tuple(xloop(parents, default_not_iterate=[str])))

    def start_cursor(self) -> Optional[_ParentCursor]:
        """

        :return:
        """
        parents = self.parents
        if not parents:
            return None
        return _ParentCursor(parent=parents[0], index=0, chain=self)


def _check_proper_cacher_or_raise_error(cacher):
    """ Checks if passed-in value is a proper cacher value from user to Config;
        otherwise we raise an error.
    """
    if cacher is Default:
        return
    if cacher is None:
        return
    if isclass(cacher) and issubclass(cacher, ProviderCacher):
        return
    ConfigError(
        f"Provided cacher ({cacher}) to Config was NOT a ProviderCacher subclass type, "
        f"`Default` or `None`"
    )


class Config(Dependency):
    """
    Lets you easily get configuration values from various sources.

    You should read [Config Class Overview](#config-class-overview) first because it's a
    high-level overview of Config. Also, read the associated [Quick Start](#quick-start) that's
    there too. What you'll find below are implementation details that go into more depth on how
    Config works in various scenarios.

    .. todo::
        At some point in the future I would like to implement __getitem__ to have the Config class
        act sort of like a dictionary. If I did that, I would like the ability to iterate
        over all the current configuration key/values that the Config object knows about.
        Doing this would be a bit involved, so for now I am leaving dict/mapping like
        access non-implemented.
    """

    # These are guaranteed to be here after __init__
    # These contain the name/value pairs for our overrides and defaults.
    _override: DirectoryListing
    _defaults: DirectoryListing

    # Set in __init__, used to know if user wants us to user parent-chain or not.
    _use_parent: bool = True

    # These are here to store info from __init__, for lazy allocation when needed;
    # and to know what the user actually wanted [ie: Default, Blank list, None, Etc].
    _cacher: Union[DefaultType, None] = Default

    # These are also from __init__ [see last comment above _cacher]
    _providers: OrderedDefaultSet[Type[Provider]]
    _directories: OrderedDefaultSet[Directory]
    _exports: OrderedDefaultSet[Directory]

    @classmethod
    def current(cls):
        """ Calls 'cls.grab()', just am alternative name for the same thing, may make things
            a bit more self-documenting, since `Config` could be used in a lot of places.

        """
        return cls.grab()

    def __init__(
            self, *,
            directories: Union[Iterable[DirectoryOrPath], DefaultType] = Default,
            providers: Union[Iterable[Type[Provider]], DefaultType] = Default,
            cacher: Union[Type[DynamoCacher], DefaultType, None] = Default,
            use_parent: bool = True,
            defaults: Union[DirectoryListing, Dict[str, Any], DefaultType] = Default,
            service: str = Default,
            environment: str = Default
    ):
        """
        Create a new Config object. Normally you would just leave everything at their Default
        values. You can change any of them if needed. If you pass a None for any parameter
        that defaults to Default, that aspect will be disabled/not used. For example, if you
        pass a None for directories/providers, no directories/providers will be searched.

        Parameters
        ---------
        directories: Union[Iterable[xcon.directory.DirectoryOrPath], xsentinels.Default]
            List of directories/paths to search when querying for a name.
            
            If `xsentinels.Default`: Uses the first one from [Parent Chain](#parent-chain).
            
            If everyone in the parent chain is set to `Default`,
            uses `xcon.conf.XconSettings.directories`.

            Various ways to change what directories to use:

            >>> from xsentinels import Default
            >>> 
            >>> with Config(directories=['/some/other/path', Default]):
            ...     # Will first look in `/some/other/path`,
            ...     # if not found next looks at default paths/directories.
            ...     assert config['some_var'] == 'expected-value'

            .. note:: This will also preserves the current service name
                As your just changing the directories used, and not the service name.
                This means the cache-path is not changed, so you don't have to add permissions
                to read/write other cache-path

            If you want to lookup the standard/default ones first, you can do this too:

            >>> my_directories = [
            ...     Default,
            ...     # Directory objects support `{environment}` and `{service}`
            ...     # placeholders in their path.
            ...     Directory(service='other_app', env='{environment}'),
            ...     Directory(service='other_app/all'),  
            ... ]
            >>> with Config(directories=my_directories):
            ...     assert config.get('some_var') == 'expected-value'

            When `Default` is resolved, after the ones your inserting your self,
            it will use the standard app service/env.

            This means it will first look for the two directiores first from other_app,
            and then if it still can't find the var it will next look at the current
            app/service for the var.

        providers: Union[Iterable[Type[xcon.provider.Provider]], xsentinels.DefaultType]
            List of provider types to use. If set to `Default`, uses the first one from
            [Parent Chain](#parent-chain). If everyone in the parent chain is set to `Default`,
            uses `xcon.conf.XconSettings.providers`.

        cacher: Type[xcon.provider.ProviderCacher]
            In the future, I may allow other cachers to be passed in via this param, but right
            now only the DynamoCacher is used and the only values you can use are:

            - If `None`:
                - No flattened high-level caching will be used. The individual
                  providers will still cache things internally per-directory/provider.

            - If left as `xsentinels.Default`:
                - Must have a service/enviroment we can use
                  (ie: APP_ENV / APP_NAME;
                      `xcon.conf.XconSettings.environment` / `xcon.conf.XconSettings.service`
                  ).
                  If so, we will attempt to read/write to a special Dynamo table that has
                  a flattened list of name/value pairs that are tied to the current service,
                  enviroment, directory-chain, provider-chain at the time the value is asked for.

            The cacher-path will use current service/environment
            (`APP_ENV` / `APP_NAME`;
            `xcon.conf.XconSettings.environment` / `xcon.conf.XconSettings.service`
            ).

            If you want change where you lookup variables without effecting the cacher-path,
            you can change the directories that Config uses.

            See below on `service` and `directories` paramters for examples/details.

        use_parent: bool
            [Parent Chain](#parent-chain) is used to find:

               - Overridden config values; these are values that set directly
                 on the Config object; ie: `xcon.config.config`.CONFIG_NAME = "Some Value to Override With"

               - Default values; these are used when config can find no other value for a
                 particular .CONFIG_NAME. See `set_default`

               - Default directories: Use parent directories by default.

               - Default providers: Use parent providers by default.

            The overridden/defaults/directory/providers 'inherit' up the config's
            [Parent Chain](#parent-chain).

            This makes it easy to override values in some parent... perhaps in a unit-test, or
            while a documentation generator is running, or if some library your calling
            wants to use a different set of providers, etc....

            If you pass a `use_parent=False`, no parent will be used or consulted. If anyone
            in the [Parent Chain](#parent-chain) has `use_parent==false`, the parent-chain
            will stop there.

            By `xsentinels.Default`:
               We lookup the parent by getting the current Config via current XContext;
               If that's ourselves, then we grab the parent context's Config resource.
               This lookup occurs every time we are asked for a .CONFIG_NAME to see if
               there is an override for it, etc. [see `parent is used to find` section above].
               That means the Config's parent can change depending on the current context the
               time the .CONFIG_NAME is asked for.

        defaults: Union[xcon.directory.DirectoryListing, Dict[str, Any], xsentinels.Default]
            If `defaults` are provided, these values will be used when Config is asked for
            something that does not exist anywhere else. ie: Has not been overridden [by directly
            setting value on Config or a parent Config], and also not in any provider.

            Basically, if Config can't find a value anywhere else, it will as a last-resort
            check these defaults. If a value in defaults is present for the configuration
            name/key in question, the value in defaults will be returned.
            This default value is NOT cached via the DynamoCacher [in-fact, the DynamoCacher will
            cache the fact that the config var in question does not exist]. If the cacher reports
            that a particular var does not exist [reminder: the cache entries eventually expire]
            we skip checking the providers and just check the defaults.

            See the Config class doc, and the 'Search Order' section.

        environment: Dict[str, Default]
            Used to easily override the `APP_ENV` / `xcon.conf.XconSettings.environment`.
            Infact, `__init__` will simply do this if you provide a value for `environment`:

            >>> self.environment = environment

            Can be used when APP_ENV is needed mainly for constructing default directory paths
            (ie: `/{APP_NAME}/{APP_ENV}/...`) but not effect the cacher's hash-key at the same
            time.
            
            The cacher always uses `xcon.conf.XconSettings.environment` for it's dynamo table
            hash-key.  That way whatever xcon_settings are looked up from alternet environment/service
            names; it won't try to write them into that apps service/enviroment cached values.
            
            It will keep the newley cached values into the `xcon.conf.XconSettings.service`
            and `xcon.conf.XconSettings.enviroment` hash-value.
            
            It will still keep track of the values in the overriden service/enviroment
            via the dynamo tables range-key, so things will still be properly/sepeatly
            cached correctly.


        service: Dict[str, Default]
            Used to easily override the `APP_NAME` / `xcon.conf.XconSettings.service`.
            Infact, `__init__` will simply do this if you provide a value for `service`:

            >>> self.service = service

            Can be used when APP_ENV is needed mainly for constructing default directory paths
            (ie: `/{APP_NAME}/{APP_ENV}/...`) but not effect the cacher's hash-key at the same
            time.
            
            The cacher always uses `xcon.conf.XconSettings.environment` for it's dynamo table
            hash-key.  That way whatever xcon_settings are looked up from alternet environment/service
            names; it won't try to write them into that apps service/enviroment cached values.
            
            It will keep the newley cached values into the `xcon.conf.XconSettings.service`
            and `xcon.conf.XconSettings.enviroment` hash-value.
            
            It will still keep track of the values in the overriden service/enviroment
            via the dynamo tables range-key, so things will still be properly/sepeatly
            cached correctly.
        """  # noqa
        super().__init__()

        self._override = DirectoryListing()
        self._defaults = DirectoryListing()

        # By default, we grab the ones from the parent chain and use them.
        self._exports: Dict[Union[DefaultType, str], None] = {Default: None}

        self._use_parent = use_parent

        # We lazy-lookup directories if it's `Default`, this is so you can directly override
        # xcon_settings.service and/or xcon_settings.environment if you want to easily change
        # the defaults.
        # See 'self.directories' property.
        self.directories = directories

        # This property will lazily be used to create self.provider_chain when the chain
        # is requested for the first time.
        self._providers = {x: None for x in xloop(providers, default_not_iterate=[str])}

        # We lazy-lookup cacher if it's Default or a Type.
        # See 'self.cacher' property.
        _check_proper_cacher_or_raise_error(cacher)
        self._cacher = cacher

        if isinstance(defaults, dict):
            for name, value in defaults.items():
                self.set_default(name, value)

        self._service = service
        self._environment = environment

    @property
    def providers(self) -> Union[DefaultType, Iterable[Union[Type[Provider], DefaultType]]]:
        """ Lets you see providers set directly on this config object.

            If set to Default, it  means we look to our [Parent Chain](#parent-chain) first,
            and if one of them don't have any set to then use sensible defaults.

            Otherwise it's a list of `xcon.provider.Provider` types and/or Default.
        """
        return self._providers.keys()

    @property
    def directories(self) -> Union[DefaultType, Iterable[Union[Directory, DefaultType]]]:
        """ Lets you see directories set directly on this config object.

            If set to Default, it  means we look to our [Parent Chain](#parent-chain) first,
            and if one of them don't have any set to then use sensible defaults.

            Otherwise it's a list of `xcon.directory.Directory` and/or Default.
        """
        return self._directories.keys()

    @directories.setter
    def directories(
            self,
            value: Union[Iterable[Union[DefaultType, DirectoryOrPath]], DefaultType]
    ):
        """ List of all directories set on self, by default it's just `[Default]`.
            This DOES NOT resolve the `Default` if it's in the list.  That's resolved
            when you ask for the `Config.directory_chain`.
        """
        # make an ordered-set out of this.
        dirs: OrderedDefaultSet = {}
        for x in xloop(value, default_not_iterate=[str]):
            if x is not Default:
                x = Directory.from_path(x)
            dirs[x] = None

        self._directories = dirs

    @providers.setter
    def providers(self, value: Union[DefaultType, Iterable[Union[DefaultType, Type[Provider]]]]):
        """ List of all providers set on self, by default it's just `[Default]`.
            This DOES NOT resolve the `Default` if it's in the list.  That's resolved
            when you ask for the `Config.provider_chain`.
        """
        # make an ordered-set out of this.
        self._providers = {x: None for x in xloop(value, default_not_iterate=[str])}

    def add_provider(self, provider: Type[Provider]):
        """ Adds a provider type to end of my provider type list [you can see what it is for
            myself via `Config.providers`].  By default, a Config object starts off with
            a provider list of just `[Default]`. By adding to the end of this, we still
            pick up the parent/default providers. This method simply appends to whatever
            we currently have.  If provider is already in list, nothing changes
            [ie: existing order will not change].
        """
        # If we already have it, no need to do anything else.
        if provider in self._providers:
            return

        # Add Provider type; using dict as an 'ordered set'; see xsentinels.OrderedSet.
        self._providers[provider] = None

    def add_directory(self, directory: Union[Directory, str, DefaultType]) -> 'Config':
        """ Adds a directory to end of my directory list [you can see what it is for
            myself via `Config.directories`].  By default, a Config object starts off with
            a directory list of just `[Default]`. By adding to the end of this, we still
            pick up the parent/default directories.  If directory is already in list,
            nothing changes [ie: existing order will not change].

            Returns self, so you can chain it.
        """
        # If we already have it, no need to do anything else.
        if directory in self._directories:
            return self

        # Add Directory; using dict as an 'ordered set'; see xsentinels.OrderedSet
        self._directories[directory] = None
        return self

    def add_export(self, *, service: str):
        """
        These are added to the `Config.directory_chain` after the normal directories from
        `Config.directories`. The purpose of these are to see 'exported' values from other
        services.  We currently use the current `xcon.conf.XconSettings.environment` or
        `Config.resolved_environment` when looking at the exported values for a service.

        Directories that are created in the `Config.directory_chain` from these exports follow
        this pattern:

        "/{service}/{environment}/export"


        By default, the export list is just this:

        ( `xsentinels.Default`, )

        When you add more exports via `Config.add_export`, it will append to the end of this list.
        The `Default` export are any exports from the parent `Config` object.
        That way we still add whatever exports are in parent and then we add the ones
        from self.

        If you want to remove the [Default] option, see `Config.set_exports`.

        .. todo:: Someday in the future, we will probably add other parameters to override
            what service to use.

        Args:
            service: Name of the service you want exported values from.
                We currently use the environment that the Config object sees. At some point in
                the future we may support also adding an explict environment here as well
                (so you don't have to use the 'current' environment name, ie: testing/prod/etc;
                you could use whatever you want).
        """
        # This is an OrderedDefaultSet, add in the service...
        self._exports[service] = None

    def set_exports(self, *, services: Iterable[Union[str, DefaultType]]):
        """
        This allows you to set all the exports. Right now
        we only support setting them by service [and not environment]. See `Config.add_export`
        for more details.

        This replaces all current services. By default, the export list is this:

        ( `xsentinels.Default`, )

        `Default` when resolved means we ask the parent chain for any exports.
        If you set the exports without including this then the parent-chain won't be consulted.

        See `Config.add_service` for a way to easily append to end of list and keeps what's
        currently in it.

        Args:
            services (Iterable[Union[str, `xsentinels.Default`]]): List of exports you want
                to add by service name. If you don't add the `xsentinels.Default` somewhere in
                this list then we will NOT check the parent-chain
        """
        self._exports = {x: None for x in xloop(services, default_not_iterate=[str])}

    def get_exports_by_service(self):
        """ List of services we currently check their export's for. This only lists the exports
            directly assigned to self (not in the [Parent Chain](#parent-chain)). Allows you to
            find out what this config object as set directly on it for which exports we
            look for per-service.
        """
        return self._exports.keys()

    def set_override(self, name, value: [Any, Default]):
        """
        Sets an override on self. When someone asks for this value, this will be returned
        regardless of what any provider or environmental variable as set.

        You can also set an override by setting a value for a config-name directly on `Config`
        via this syntax:

        >>> from xcon.config import config
        >>> config['some_override_name'] = "my override value"

        For details see [Naming Rules](#naming-rules).

        .. important:: This will also affect child config objects!
            They will look for overrides set on a parent before looking at any providers.

            For more details see [Parent Chain](#parent-chain) and [Overrides](#overrides) topics.

        Args:
            name: Name of the item to remove, case-insensitive.
            value (Union[Any, xsentinels.Default]): Can be any value. If Default is used
                we will instead call `Config.remove_override(name)` for you to remove the value.
        """
        if value is Default:
            self.remove_override(name)
            return

        # Checked for default already, from this point forward it's just `Any` type.
        value: Any
        override_item = DirectoryItem(
            directory="/_override", name=name, value=value, source=f"Config.set_override",
            cacheable=False
        )

        xlog.info(f"Setting Config override for item ({override_item}).")
        self._override.add_item(
            override_item
        )

    def get_override(self, name) -> Union[Any, DefaultType, None]:
        """
        Returns a value of override for `name` was directly set on this config object
        in one of two ways:

        - `config.set_override`
        -  `config.SOME_VAR = "a-value"`

        The returned value is `xsentinels.Default` if no override is found;
        this is so you can distinguish between overriding to None or no override set at all
        (`xsentinels.Default` evaluates to `False`, just like how `None` works).

        .. warning:: Only returns a value if overrides was directly set on self!

            (ie: **won't** consult the [Parent Chain](#parent-chain)).
            The parent chain (parent configs) are consulted when looking up a config value
            normally via `Config.get`. Overrides in self and then in parents are checked first.

            `get_override` method is here so you can examine a specific `Config` object and
            determine if there are any overrides set directly on it.

        Attributes:
            name (str): Name to use to get override [case-insensitive].

        Returns:
            Union[Any, xsentinels.Default]: The value, or `xsentinels.Default` if no value was
                set for `name`. This allows you to distinguish between overriding a value to
                `None` and no override being set in the first place
                (`xsentinels.Default` evaluates to `False`, just like how `None` works).
        """
        item = self._override.get_item(name)
        if item:
            return item.value
        return Default

    def remove_override(self, name):
        """
        Remove override **ONLY** on self.
        This will not remove overrides from a parent.

        .. warning:: This WON'T affect any override set on a parent!
            see [Parent Chain](#parent-chain).

        Someday we may make it easier to publicly go through the parents
        (right now there are internal/private methods that do this).

        If you don't like an override, you can override the override by setting an override
        on a child/current `Config` object (see `Config.set_override`).

        Probably should not mess with config objects from higher up that you don't know
        anything about in any case.  That's why I've hesitated about publicly exposing
        the parent chain too much.


        You can remove overrides in various ways, such as:

        ```python
        from xcon import config
        from xsentinels import Default

        # Alternate Method 1:
        config.SOME_NAME = Default

        # Alternate Method 2:
        config.set_override("SOME_NAME", Default)
        ```

        At the moment these ways ^ will not remove an override from a parent.

        If we do decide we want an ability  to "white-out" an override;
        I would probably do it such that you could tell a child to not check parent(s)
        overrides on a specific value
        (ie: I would set the override value to `Default` on self internally, to indicate that).
        """
        xlog.info(f"Removing Config override for name ({name}).")
        self._override.remove_item_with_name(name=name)

    @property
    def service(self) -> Union[DefaultType, str]:
        """
        Returns the value passed into __init__ for `service` or set on property `Config.service`.

        ```python
        # Ways to override service on a Config object.
        config.service = "some-service"
        config = Config(service="some-service")
        ```

        If it was not overridden, then returns `Default`. This is a placeholder that represents
        the default value should be used. You can set the service via `config.service = Default`
        to remove any overridden and return to having it lookup the service like normal
        from any parent(s) or `xcon.conf.XconSettings.service`.

        If your interested it knowing what it's current using as the service name, you could
        use `Config.resolved_service`. It will return the currently resolved service name
        that is currently being used for that config object.
        """
        return self._service

    @property
    def environment(self) -> Union[str, DefaultType]:
        """
        Returns the value passed into __init__ for `environment` or set on property
        `Config.environment`.

        ```python
        # Ways to override service on a Config object.
        config.environment = "some-env"
        config = Config(environment="some-env")
        ```

        If it was not overridden, then returns `Default`. This is a placeholder that represents
        the default value should be used. You can set the service via
        `config.environment = Default` to remove any overridden and return to having it lookup the
        service like normal from any parent(s) or `xcon.conf.XconSettings.environment`.

        If your interested it knowing what it's current using as the service name, you could
        use `Config.resolved_environment`. It will return the currently resolved environment name
        that is currently being used for that config object.
        """
        return self._environment

    @environment.setter
    def environment(self, value):
        """ See `Config.environment getter for details. """
        self._environment = value

    @service.setter
    def service(self, value):
        """ See `Config.service getter for details. """
        self._service = value

    @property
    def resolved_service(self):
        """
        Will check self (`Config.service`), the [Parent Chain](#parent-chain) and finally
        `xcon.conf.XconSettings.service` for the current value this Config object should
        use when resolving directory paths to lookup config values.
        """
        return self._service_with_cursor(cursor=self._parent_chain().start_cursor())

    @property
    def resolved_environment(self):
        """
        Will check self (`Config.environment`), the [Parent Chain](#parent-chain) and finally
        `xcon.conf.XconSettings.environment` for the current value this Config object should
        use when resolving directory paths to lookup config values.
        """
        return self._environment_with_cursor(cursor=self._parent_chain().start_cursor())

    @property
    def cacher(self) -> Optional[Union[Type[DynamoCacher], DefaultType]]:
        """ Returns what was originally passed into __init__ for `cacher`.
            It's here, so you can see how this Config object was originally configured.
        """
        return self._cacher

    @cacher.setter
    def cacher(self, value: Union[Type[DynamoCacher], DefaultType, None]):
        """ Returns what was originally passed into __init__ for `cacher`.
            It's here so you can see how this Config object was originally configured.
        """
        _check_proper_cacher_or_raise_error(value)
        self._cacher = value

    @property
    def resolved_cacher(self) -> Optional[ProviderCacher]:
        """
        Returns the cacher object that is currently being used when config values are asked
        for; at the time this is called.
        This can change later if a config object or current context is changed.

        If environmental variable CONFIG_ONLY_ENV is set to true (looked at via `os.environ` ONLY),
        we will only ever return None for the resolved cacher to use no matter what
        parent-config / self's `Config.cacher` have been set with.
        """
        return self._cacher_with_cursor(cursor=self._parent_chain().start_cursor())

    @property
    def provider_chain(self) -> ProviderChain:
        """
        `xcon.provider.ProviderChain` we are currently using.
        This is effected by what was passed into Config when it was created.
        If it was left as `xsentinels.Default`, we will get the value via the
        [Parent Chain](#parent-chain).

        See `Config` for more details.

        If environmental variable CONFIG_ONLY_ENV is set to true (looked at via `os.environ` ONLY),
        we will only have the EnvironmentalProvider in the used/returned provider chain
        no matter what parent-config / self's `Config.providers` have been set with.
        """
        return self._provider_chain_with_cursor(cursor=self._parent_chain().start_cursor())

    @property
    def directory_chain(self) -> DirectoryChain:
        """
        `xcon.directory.DirectoryChain` we are currently using.
        This is effected by what was passed into Config when it was created.
        If it was left as `xsentinels.Default`, we will get the value via the
        [Parent Chain](#parent-chain).

        See `Config` for more details.
        """
        return self._directory_chain_with_cursor(cursor=self._parent_chain().start_cursor())

    @property
    def use_parent(self) -> bool:
        """
        If `True`: we will use the [Parent Chain](#parent-chain) when looking up things such as the
        `Config.provider_chain`. as an example; if it was left as `xsentinels.Default`
        when `Config` object was created.

        If `False`: the parent will not be consulted, and anything that was not set at creation
        or added/set after creation will use the default values. See `Config` for details.

        See [Parent Chain](#parent-chain) for more details about how config-parents works.
        """
        return self._use_parent

    def set_default(self, name: str, value: Optional[Any]):
        """
        When someone tries to look up a config value [perhaps via `Config.get`] and if a value
        is not found anywhere... But someone called this to define a default for it we return
        the default value set here [or passed in via Config.__init__(defaults={...})].

        For a few examples of how this can be used, see `Config`, Search Order section.
        Also see Config.__init__(...) doc for 'defaults' param.

        Args:
            name (str): Case-insensitive name for the default config.
            value (Optional[Any]): Default value, can be anything [but are generally strings].
                If you provide None for this value param, that will be stored and will be returned
                if a default is needed for param [you can use this feature to override a
                parent-config default to None if needed].

        """
        if value is Default:
            self.remove_default(name)
            return

        default_item = DirectoryItem(
            directory="/_default/user-set", name=name, value=value, source=f"config.set_default",
            cacheable=False
        )

        xlog.info(
            "Setting Config default for item ({default_item}).",
            extra=dict(default_item=default_item)
        )
        self._defaults.add_item(default_item)

    def get_default(self, name: str) -> Optional[Any]:
        """
        Returns the default for 'name' if it was set via `Config.set_default()`. It only returns
        one if it was directly set on self. This means it **WON'T** consult
        the [Parent Chain](#parent-chain)). This is so you can more easily find/set defaults
        in the parent-chain your self if you need to track something down or some such.

        Most of the item you should just be able to use `Config.set_default` and not worry about
        an existing default set on us or some other `Config` object.

        Attributes:
            name (str): Name to use to get default [case-insensitive].

        Returns:
            Union[Any, None, xsentinels.Default]: The value, or Default if no default
                is set for name. This allows you to distinguish between defaulting a value to
                None and no default being set in the first place (`xsentinels.Default`
                looks like `False`, just like how `None` works).
        """
        item = self._defaults.get_item(name)
        if item:
            return item.value
        return Default

    def remove_default(self, name):
        """
        Remove default on self.

        .. warning:: This WON'T affect any default set on a parent,
            see [Parent Chain](#parent-chain).

        You can also call this other ways, such as:

        ```python
        from xcon import config
        from xsentinels import Default

        # Alternate Method 1:
        config.set_default("SOME_NAME", Default)
        ```
        """
        xlog.info(f"Removing Config default for name ({name}).")
        self._defaults.remove_item_with_name(name=name)

    def get(
            self,
            name: str,
            default=None,
            *,
            skip_providers: bool = False,
            skip_logging: bool = False,
            ignore_local_caches: bool = False
    ) -> Optional[str]:
        """
        Similar to dict.get(), provide name [case-insensitive] and we call `Config.get_item()`
        and return the `xcon.directory.DirectoryItem.value` of the item returned,
        or passed in `default=None` if no item was found.

        See documentation for `Config.get_item()` for more details and to find out more
        about the `skip_providers` option.

        Attributes:
            name (str): Name of the config value to lookup. Name can be of any case since
                lookup is case-insensitive. But if you can keep it all lower case it could be
                a bit more efficient, since it would not have to change it.

            default: Value to return if we don't find a config value via the normal means.

            skip_providers (bool): See self.get_items() for more details, suffice it to
                say it only returns things directly set/overridden or defaulted on self or on
                parent-chain [without consulting the providers and directories].

            skip_logging (bool): Skips logging about where we got the config value.

            ignore_local_caches (bool): allows you to ignore the local memory cache
                (as a convenience option).

                Right now it does this by resetting the entire cache for you before lookup.
                But in the future if needed, it may be more precise about what it does and may just
                retrieve that specific value from each provider until it finds the value
                (vs resetting the entire cache and bulk retrieving everything all over again).

                Mostly depends on how often we would really need to do this in the future.
                I am guessing it would be rare so the current implementation should be good enough
                for now.
        """
        if ignore_local_caches:
            InternalLocalProviderCache.grab().reset_cache()

        item = self.get_item(name=name, skip_providers=skip_providers, skip_logging=skip_logging)
        if item:
            value = item.value
            return value if value is not None else default
        return default

    def get_bool(self, name: str, default=False):
        """
        Grabs config variable for `name` and does it's best to convert it to a boolean if possible.
        If the value is:

        - None: we return `default`.
        - str: We run it though `distutils.util.strtobool` to convert it to a bool.
        - Any: Anything else, we simply call `bool(value)` on it.
        - If there is any ValueError while try to convert value, we return False.

        Args:
            name (str): Name of the config value, such as `DISABLE_DB`.
            default: If we can't find a value, what should we use? By default, it's False.

        Returns:
            bool:  We found a bool value, or you provided a boolean `default` value.
            default: This means that we could not find a config value, so return `default`;
                which defaults to False
        """
        value = self.get(name)
        if value is None:
            return default
        return bool_value(value)

    def get_value(self, *args, **kwargs) -> Optional[str]:
        """
        .. deprecated:: Deprecated in favor of using `Config.get()`.
            Right now we simply call `Config.get()` with same arguments for you and return result.
        """
        return self.get(*args, **kwargs)

    def get_item(
            self, name: str, *,
            skip_providers: bool = False,
            skip_logging: bool = False,
    ) -> Optional[DirectoryItem]:
        """
        Gets a DirectoryItem for name. If the value does not exist, we will still return a
        `xcon.directory.DirectoryItem` with a
        `xcon.directory.DirectoryItem.value` == `None`. This is because we cache the
        non-existence of items for performance reasons. This allows you to see
        where the None value came from via the `xcon.directory.DirectoryItem.directory`
        attribute.

        Attributes:
            name (str): Name to look for [will be used in a case-insensitive manner].

            skip_providers (bool): If False [default], checks all sources for the config
                values. If True, only checks for things overridden on self or a parent;
                [ie: Things directory set on self or directly on a parent Config].
                It will consult any defaults (`Config.get_default()`) if needed.

            skip_logging (bool): Skips logging about where we got the config value.

        Returns (Optional[DirectoryItem]):
            If None [only happens when skip_providers is True]; then no override/default was found.

            Otherwise, returns the item as a DirectoryItem. A DirectoryItem.value can be None.
            This means that the value is None [either it could not find it or the value
            was really set to a `None`].
        """
        # todo: Someday, use a special str subclass that will indicate that it's already in
        #       lower-case format and use that instead [therefore, we can skip lower-casing it
        #       again and again as we pass the already lower-cased name along to other methods].
        name = name.lower()

        # Otherwise, we follow standard process.
        return self._get_item(
            name=name,
            skip_providers=skip_providers,
            cursor=self._parent_chain().start_cursor(),
            skip_source_logging=skip_logging
        )

    def _providers_with_cursor(self, cursor: Optional[_ParentCursor]) -> List[Provider]:
        pass

    def _resolve_providers_with_cursor(
            self, cursor: Optional[_ParentCursor]
    ) -> OrderedSet[Type[Provider]]:
        if _env_only_is_turned_on():
            # We also disable cacher, see other place we call the `_env_only_is_turned_on` method.
            return {EnvironmentalProvider: None}

        return self._resolve_attr_values_with_cursor(
            cursor=cursor,
            attribute_name="_providers",
            # Default to xcon_settings.providers if there are any,
            # otherwise just the EnvironmentalProvider:
            defaults_factory=lambda: xcon_settings.providers or [EnvironmentalProvider]
        )

    def _resolve_directories_with_cursor(
            self,
            cursor: Optional[_ParentCursor],
            service: Union[str, DefaultType, None] = Default,
            environment: Union[str, DefaultType, None] = Default
    ) -> OrderedSet[Directory]:
        # Making it a callable to make it lazy.

        def defaults_factory():
            return self._standard_directories(
                cursor=cursor,
                service=service,
                environment=environment
            )

        directories = self._resolve_attr_values_with_cursor(
            cursor=cursor,
            attribute_name="_directories",
            defaults_factory=defaults_factory
        )

        exported = self._resolve_attr_values_with_cursor(
            cursor=cursor,
            attribute_name="_exports",
            defaults_factory=tuple  # Fast, empty iterable
        )

        if service is Default:
            service = self._service_with_cursor(cursor)
        if environment is Default:
            environment = self._environment_with_cursor(cursor)

        if exported:
            # Any new values will be added to end, nothing will happen to order of existing ones.
            for x in exported:
                directories[Directory(service=x, env=environment, is_export=True)] = None

        directories = {
            k.resolve(service=service, environment=environment): None for k in directories
        }

        return directories

    def _resolve_attr_values_with_cursor(
            self,
            cursor: Optional[_ParentCursor],
            attribute_name: str,
            defaults_factory: Callable[[], Iterable[T]]
    ) -> OrderedSet[T]:
        """ Internally, we are using a dict as an ordered-set, python 3.7 guarantees dicts
            keep their insertion order.

            This will return an ordered-dict, where the keys are the values. This function
            will resolve any 'Default' values encountered with their parent version.
        """
        default = Default

        values: OrderedDefaultSet[T] = getattr(self, attribute_name)
        if default not in values:
            # Ensure we don't accidentally modify this ordered set somewhere else.
            return copy(values)

        if cursor:
            parent_values = cursor.parent._resolve_attr_values_with_cursor(
                cursor=cursor.next_cursor(),
                attribute_name=attribute_name,
                defaults_factory=defaults_factory
            )
        else:
            parent_values = {x: None for x in defaults_factory()}

        # We have a default we need to 'insert' our parent providers into....
        # First we check to see if we only have 'Default'...

        if len(values) == 1:
            # If we only have one value, it's only value is 'Default', so easy, just return
            # what we have and move on.
            return parent_values

        # If we have more then just 'Default', we replace it with parent providers...
        final_values: OrderedSet[T] = {}
        for p in values:
            if p is default:
                final_values.update(parent_values)
            else:
                final_values[p] = None

        return final_values

    def _provider_chain_with_cursor(self, cursor: Optional[_ParentCursor]) -> ProviderChain:
        """
        I first check to see if I have/use a parent and our provider list is Default;
        if that's the case we return the parent Config's provider_chain.

        Otherwise we need to create a provider_chain and cache/return that now and in the future.
        """

        # Note from Josh: Keep things simple for now, just create provider chain when needed.
        #
        # if we find creating/finding the provider chain takes to long, then we can cache
        # the chains by provider values. We could also keep them around and reset them if
        # any of our parent's providers are changed, etc. I decided that it's probably not
        # expensive and so not to pre-optimize and just worry about it down the road if that's
        # not the case anymore.

        provider_types = self._resolve_providers_with_cursor(cursor=cursor)
        return ProviderChain(providers=provider_types)

    def _cacher_with_cursor(
            self,
            cursor: Optional[_ParentCursor]
    ) -> Optional[DynamoCacher]:
        # if user set cacher to None, they don't want caching enabled, so return None.
        cacher = self._cacher
        if cacher is None:
            return None

        # if user wants to force only the environmental provider to be used, disable cacher too.
        if _env_only_is_turned_on():
            return None

        # If we have a parent, and user wants the Default cacher, ask the parent for it.
        if cursor and cacher is Default:
            return cursor.parent._cacher_with_cursor(cursor=cursor.next_cursor())

        # If cacher is Default, right now the only supported cacher type is DynamoCacher.
        if cacher is Default:
            # We don't check self, only an environmental variable for this.
            # This is so you don't have to modify the code to disable cacher by default.
            # If you want to disable cacher via code, do this instead:
            #
            #  with Config(cacher=None):
            #      pass
            #
            # or
            #
            #  @Config(cacher=None)
            #  def some_method():
            #      pass
            #
            # BUT if someone passes `Config(cacher=DynamoCacher)` explicitly we will use that
            # regardless of what `XCON_DISABLE_DEFAULT_CACHER` is set too.
            #
            # Lower-casing it because `EnvironmentalProvider` will do that for us when looking it
            # up (it looks it up in a case-insensitive manner).
            # Trying to make it a tiny bit more efficient since this is called a lot.
            env_provider = EnvironmentalProvider.grab()
            if xcon_settings.disable_default_cacher:
                return None
            cacher = DynamoCacher

        # We only accept types at this point [we already handled None and Default cases above ^],
        # the idea is to use it as a resource, so that we can have multiple config objects
        # use the same 'cacher' type.  Right now, only DynamoCacher is even supported,
        # this is more of a sanity check. If we ever have other ProviderCacher subclasses in
        # the future, we can update this to be more open [but should check for inspect.isclass
        # in that future, we want only class types from the user at this point].
        if cacher is not DynamoCacher:
            raise ConfigError(
                f"Trying to get the cacher, but the type the user wants to use is not a "
                f"DynamoCacher type: ({cacher})  In the future I may support other cacher "
                f"types; but right now we only support either None, Default or "
                f"xcon.providers.dynamo.DynamoCacher."
            )

        # Grab the current cacher resource, it's a ProviderCacher type of some sort and
        # so is a xinject.dependency.Dependency
        # (right now, cacher can only be a DynamoCacher type;
        # although we can change that in the future if we decide to change how caching works)
        return cacher.grab()

    def _resolve_attr_with_cursor(
            self,
            cursor: Optional[_ParentCursor],
            attribute_name: str,
            defaults_factory: Callable[[], T]
    ) -> T:
        # if user set cacher to None, they don't want caching enabled, so return None.
        value = getattr(self, attribute_name)
        if value is not Default:
            return value

        if not cursor:
            # We don't have any more parents to check, get the default value via factory callable.
            return defaults_factory()

        # If we have a parent via the next-cursor, use that to see if we can find a value.
        return cursor.parent._resolve_attr_with_cursor(
            cursor=cursor.next_cursor(),
            attribute_name=attribute_name,
            defaults_factory=defaults_factory
        )

    def _get_item(
            self, name: str, *,
            skip_providers: bool = False,
            skip_defaults: bool = False,
            cursor: Optional[_ParentCursor],
            skip_source_logging: bool = False,
    ) -> Optional[DirectoryItem]:

        item = None
        provider_chain = None
        directory_chain = None
        try:
            item = self._override.get_item(name)
            if not item and cursor:
                item = cursor.parent._get_item(
                    name=name,
                    skip_providers=True,
                    skip_defaults=True,
                    cursor=cursor.next_cursor(),
                    skip_source_logging=skip_source_logging
                )

            if item:
                return item

            # Check to see if we are skipping providers, this happens when we only want to look
            # at overrides and/or default values to fulfill the request. This mostly only happens
            # for 'app_env' and 'service_name' config vars.
            if not skip_providers:
                # We skip logging about this, since we normally don't care... Just a lot of extra
                # useless log messages about where we keep getting the service or environment from.
                service = self._service_with_cursor(cursor)
                environment = self._environment_with_cursor(cursor)

                cacher = None

                # We will disable caching if we don't have a defined service (ie: 'global' service)
                if not service or service != "global":
                    cacher = self._cacher_with_cursor(cursor=cursor)

                directory_chain = self._directory_chain_with_cursor(
                    cursor=cursor,
                    service=service,
                    environment=environment
                )
                provider_chain = self._provider_chain_with_cursor(cursor=cursor)

                cache_dir = None
                use_cacher = bool(cacher and provider_chain.have_any_cachable_providers)
                if use_cacher:
                    # todo: Consider passing this into _directory_chain_with_cursor instead
                    #   of the individual components.
                    cache_dir = Directory.from_components(service=service, environment=environment)
                else:
                    cacher = None

                item = provider_chain.get_item(
                    name=name,
                    directory_chain=directory_chain,
                    cacher=cacher,
                    environ=cache_dir
                )

            if skip_defaults:
                # If they skip defaults then return None...
                # with skip_defaults == False: We return a DirectoryItem with None as the value.
                # todo: Double check to see if we want to return item vs None
                #   [adjust comment just above to reflect decision].
                return item

            if not item or (item.value is None and item.directory.is_non_existent):
                # Check for default values next...
                default_item = self._get_default_item_with_cursor(name=name, cursor=cursor)
                if default_item:
                    item = default_item
        finally:
            # We normally only want to log about things the users actually requests,
            # and not things used to fulfill the request.
            if not skip_source_logging:
                self._log_about_item_retrieval(name, item, directory_chain, provider_chain)

        return item

    # noinspection PyMethodMayBeStatic
    def _log_about_item_retrieval(
        self,
        original_name: str,
        item: DirectoryItem,
        directory_chain: DirectoryChain = None,
        provider_chain: ProviderChain = None
    ):
        env_only_enabled = _env_only_is_turned_on()

        if item and not item.directory.is_non_existent:
            # FYI: What's nice about doing it this way are these string formatting placeholders
            # will only evaluated if log message is actually emitted.
            # This logging message could be called a lot.
            xlog.debug(
                "Config found ({config_var_name}); returned {config_item}; extra metadata {meta}; "
                f"env_only_enabled({env_only_enabled}).",
                extra=dict(
                    config_var_name=original_name,
                    config_item=item,
                    meta=item.supplemental_metadata,
                )
            )
        elif item:
            directories = directory_chain.directories if directory_chain else []
            providers = provider_chain.providers if provider_chain else []

            supplemental_msg = ""
            if item.from_cacher:
                supplemental_msg = f"non-existence entry was found in cacher; "

            xlog.debug(
                "Config not found ({config_var_name}); "
                f"{supplemental_msg}"
                "in directories ({directories}), "
                "for providers ({providers}); extra metadata ({meta}); "
                f"env_only_enabled({env_only_enabled}).",
                extra=dict(
                    config_var_name=original_name,
                    directories=[d.path for d in directories],
                    providers=[p.name for p in providers],
                    source=item.source,
                    meta=item.supplemental_metadata,
                )
            )

    def _get_default_item_with_cursor(
            self, name: str, cursor: Optional[_ParentCursor]
    ) -> Optional[DirectoryItem]:
        item = self._defaults.get_item(name=name)
        if item:
            return item

        if cursor:
            return cursor.parent._get_default_item_with_cursor(
                name=name,
                cursor=cursor.next_cursor()
            )

        return None

    def _parent_chain(self) -> _ParentChain:
        """
            See [Parent Chain](#parent-chain).

            There is a concept of a parent-chain with Config if the Config object has
            their `use_parent == True` [it defaults to True]. We use the current XContext to
            construct this parent-chain. See [parent-chain].

            The parent-chain starts with the config resource in the current XContext.
            If that context has a parent context, we next grab the Config resource from
            that parent context and check it's `Config.use_parent`. If True we keep doing
            this until we reach a XContext without a parent or a `Config.use_parent` that is False.

            We take out of the chain any config object that is myself. The only objects in
            the chain are other Config object instances.

            The parent chain is generally consulted when we encounter a `Default` value.
            If when reaching the last parent in the chain, we still have a `Default` value,
            sensible/default values are constructed [if they have not already been] and used.
        """
        use_parent = self._use_parent
        found_self = False
        skip_adding_more_parents = False

        chain = []
        for config_resource in XContext.grab().dependency_chain(Config, create=True):
            if config_resource is self:
                found_self = True
                if not use_parent:
                    break
                continue

            if skip_adding_more_parents:
                continue

            chain.append(config_resource)
            if not config_resource.use_parent:
                skip_adding_more_parents = True
                if use_parent:
                    # We don't need to go any further, we want to use parents and
                    # we found a parent that does not, just return what we have.
                    break
                # We keep going to see if we can find our selves, but we don't add
                # anymore to the chain. If we can't find our selves as a resource
                # in the current context parent-chain, we will return a blank chain.
                continue

        # If we did not find self and we don't want to use a parent, we return a blank parent
        # chain. This is because we are not in the current config context hierarchy, and so are
        # a 'separate' island and are cut-off from all other config objects.
        if not use_parent and not found_self:
            return _BlankParentChain

        return _ParentChain(parents=tuple(chain))

    def _directory_chain_with_cursor(
            self,
            cursor: Optional[_ParentCursor],
            service: Union[str, DefaultType, None] = Default,
            environment: Union[str, DefaultType, None] = Default,
    ) -> Optional[DirectoryChain]:
        directories = self._resolve_directories_with_cursor(
            cursor=cursor,
            service=service,
            environment=environment
        )

        return DirectoryChain(directories=directories)

    def _standard_directories(
            self,
            cursor: Optional[_ParentCursor],
            service: Union[str, DefaultType, None] = Default,
            environment: Union[str, DefaultType, None] = Default
    ) -> OrderedSet[Directory]:
        if service is Default:
            service = self._service_with_cursor(cursor)
        if environment is Default:
            environment = self._environment_with_cursor(cursor)

        return {
            d.resolve(service=service, environment=environment): None
            for d in xloop(xcon_settings.directories, default_not_iterate=[str])
        }

    def _service_with_cursor(self, cursor: Optional[_ParentCursor]) -> str:
        return self._resolve_attr_with_cursor(
            cursor=cursor,
            attribute_name='_service',
            defaults_factory=lambda: xcon_settings.service or 'global'
        )

    def _environment_with_cursor(self, cursor: Optional[_ParentCursor]) -> str:
        return self._resolve_attr_with_cursor(
            cursor=cursor,
            attribute_name='_environment',
            defaults_factory=lambda: xcon_settings.environment or 'all'
        )

    def _get_special_non_provider_item_with_cursor(
            self,
            name: str,
            hard_default: str,
            cursor: Optional[_ParentCursor],
            *,
            skip_source_logging: bool = False,
            as_item: bool = False
    ) -> Union[DirectoryItem, str]:
        """
        Returns an item/value by searching for 'name' without using providers.
        If we can't find a value, or the value we find is false we use the provided hard_default.

        Args:
            name: Name to look for [case-insensitive]
            hard_default: Default to use if we can't find a non-false like value.
            cursor: Current cursor we are using, see `_ParentCursor` for more details.
            skip_source_logging: Don't log where we got this from
                (if this is used to get something else, limits excessive logging].
            as_item: Return `DirectoryItem` if true, else we return just the value.
        """
        # We need to use internal method to preserve the cursor.
        item = self._get_item(
            name,
            skip_providers=True,
            cursor=cursor,
            skip_source_logging=skip_source_logging
        )

        if item and item.value:
            if as_item:
                return item
            return item.value

        item = EnvironmentalProvider.grab().get_item_without_environ(name)
        if not item or not item.value:
            item = DirectoryItem(
                directory="/_default/hard-coded",
                name=name,
                value=hard_default,
                source=f"config.hard-coded-default={hard_default}",
                cacheable=False
            )

        return item if as_item else item.value

    def __getitem__(self, key):
        """ Allows config to be used like a dictionary, in that you can look up config values
            via the `config['some_key']` syntax.
        """
        return self.get(key)

    def __setitem__(self, key, value):
        """
        Allows one to override values on config when used like a dictionary,
        in that you can set override values on config values via the
        `config['some_key'] = 'override-with-value'` syntax.

        To remove an override you can either set the value to `Default`;
         ie: `config['some_key'] = Default`.

        Or call `Config.remove_override`; ie: `config.remove_override('some_key')`.
        """
        self.set_override(key, value)


# Allocate a ready-to-go default config object, see documentation above near start of file
# for details on what this is and how to use it.
#
# noinspection PyRedeclaration
config = Config.proxy()
"""
This will be an alias for the current Config object. Every time you ask it for something,
it looks up the current Config object and gets it from that. This means you can use this
directly as-if it's a `Config` object. Anytime you use it, it will lookup the current config
object and use that to get the attribute/method you want.

Example use case:

```python
    from xcon import config
    value = config.SOME_VAR
```
"""

Config.grab()


def _env_only_is_turned_on() -> bool:
    # Check xcon_settings to see if env-only is  turned on.
    return xcon_settings.only_env_provider


# todo: remove this function, unused now.
# todo: Remove and move doc comment
def _replace_standard_directories(*, service: str, env: str) -> OrderedSet[Directory]:
    """
    Gives you the standard list of directories for service/env combination.
    This is called when creating a `Config.__init__` if no `directories` are passed into any
    Config object in the [Parent Chain](#parent-chain).

    Normally you would want to use Config.directories = [Default]; which would cause the
    Config object to ask it's parent chain, and they are also all set to `Default` then
    it will call this `standard_directories` method for you to get the `Default` directories.

    If for some reason you want to custimize or use diffrent service/env names for addtional
    directory paths to check beyond the defaults, you can call this method and append them to
    a config object, like so:

    >>> from xcon.config import config
    >>> for directory in standard_directories(service="customService", env="customEnv"):
    ...     config.add_directory(directory)

    The code ^ above would keep the default directory as the first one(s) to look at to the
    current/default config. After looking at the pre-existing default ones it would next look
    at your customService/customEnv paths too afterwards. Finally, Config would then look at
    any ones added via `Config.add_export`.

    ## Whats Returned Summary

    For more details and context surounding what is returned, see
    [Standard Directory Paths](#standard-directory-paths). Below is a summary.

    As a side-note: you can change the default directories via
    `xcon.conf.XconSettings.directories`,
    so if that changes then what this returns will also change.

    Right now we return these directories by default, in priority order:

    1. `/{service}/{env}`
    2. `/{service}/all`
    3. `/global/{env}`
    4. `/global/all`

    If service == 'global' or None or blank, we will use `global` and only provide the two
    global directories and leave the {service} ones out of it:

    1. `/global/{env}`
    2. `/global/all`

    If there is no `env` avaliable, `env` will just only use `../all`

    Parameters
    -----------
    service: str
        The name of the service, generally the project's name, in camelCase.
        You can grab the current service being used via `Config.SERVICE_NAME`.

        If None/Blank, we will use `global`.

    env: str
        Environment, could be 'testing', 'prod', or something custom like 'yourName',
        this is also generally in `camelCase`.

        You can grab the current environment being used via `Config.resolved_environment`.

        If None/Blank, we will use `all`.
    """
    # todo: remove this method eventually; for now keeping it so I can
    #   leave it's doc-comment in-tact. I may want to use the doc-comment somewhere else.
    raise NotImplemented("Deprecated/Unused")


_BlankParentChain = _ParentChain()


class ConfigRetriever(SettingsRetrieverProtocol):
    """Retrieving the setting from config"""
    def __call__(self, *, field: SettingsField, settings: 'XconSettings') -> Any:
        return config.get(field.name)


class ConfigSettings(Settings, default_retrievers=[ConfigRetriever()]):
    pass

Global variables

var config

This will be an alias for the current Config object. Every time you ask it for something, it looks up the current Config object and gets it from that. This means you can use this directly as-if it's a Config object. Anytime you use it, it will lookup the current config object and use that to get the attribute/method you want.

Example use case:

    from xcon import config
    value = config.SOME_VAR

Classes

class Config (*, directories: Union[Iterable[Union[Directory, str]], DefaultType] = Default, providers: Union[Iterable[Type[Provider]], DefaultType] = Default, cacher: Union[Type[DynamoCacher], DefaultType, ForwardRef(None)] = Default, use_parent: bool = True, defaults: Union[DirectoryListing, Dict[str, Any], DefaultType] = Default, service: str = Default, environment: str = Default)

Lets you easily get configuration values from various sources.

You should read Config Class Overview first because it's a high-level overview of Config. Also, read the associated Quick Start that's there too. What you'll find below are implementation details that go into more depth on how Config works in various scenarios.

TODO

At some point in the future I would like to implement getitem to have the Config class act sort of like a dictionary. If I did that, I would like the ability to iterate over all the current configuration key/values that the Config object knows about. Doing this would be a bit involved, so for now I am leaving dict/mapping like access non-implemented.

Create a new Config object. Normally you would just leave everything at their Default values. You can change any of them if needed. If you pass a None for any parameter that defaults to Default, that aspect will be disabled/not used. For example, if you pass a None for directories/providers, no directories/providers will be searched.

Parameters

directories : Union[Iterable[DirectoryOrPath], xsentinels.Default]

List of directories/paths to search when querying for a name.

If xsentinels.Default: Uses the first one from Parent Chain.

If everyone in the parent chain is set to Default, uses XconSettings.directories.

Various ways to change what directories to use:

from xsentinels import Default

with Config(directories=['/some/other/path', Default]): … # Will first look in /some/other/path, … # if not found next looks at default paths/directories. … assert config['some_var'] == 'expected-value'

Note: This will also preserves the current service name

As your just changing the directories used, and not the service name. This means the cache-path is not changed, so you don't have to add permissions to read/write other cache-path

If you want to lookup the standard/default ones first, you can do this too:

my_directories = [ … Default, … # Directory objects support {environment} and {service} … # placeholders in their path. … Directory(service='other_app', env='{environment}'), … Directory(service='other_app/all'),
… ] with Config(directories=my_directories): … assert config.get('some_var') == 'expected-value'

When Default is resolved, after the ones your inserting your self, it will use the standard app service/env.

This means it will first look for the two directiores first from other_app, and then if it still can't find the var it will next look at the current app/service for the var.

providers : Union[Iterable[Type[Provider]], xsentinels.DefaultType]
List of provider types to use. If set to Default, uses the first one from Parent Chain. If everyone in the parent chain is set to Default, uses XconSettings.providers.
cacher : Type[ProviderCacher]

In the future, I may allow other cachers to be passed in via this param, but right now only the DynamoCacher is used and the only values you can use are:

  • If None:

    • No flattened high-level caching will be used. The individual providers will still cache things internally per-directory/provider.
  • If left as xsentinels.Default:

    • Must have a service/enviroment we can use (ie: APP_ENV / APP_NAME; XconSettings.environment / XconSettings.service ). If so, we will attempt to read/write to a special Dynamo table that has a flattened list of name/value pairs that are tied to the current service, enviroment, directory-chain, provider-chain at the time the value is asked for.

The cacher-path will use current service/environment (APP_ENV / APP_NAME; XconSettings.environment / XconSettings.service ).

If you want change where you lookup variables without effecting the cacher-path, you can change the directories that Config uses.

See below on service and directories paramters for examples/details.

use_parent : bool

Parent Chain is used to find:

  • Overridden config values; these are values that set directly on the Config object; ie: config.CONFIG_NAME = "Some Value to Override With"

  • Default values; these are used when config can find no other value for a particular .CONFIG_NAME. See set_default

  • Default directories: Use parent directories by default.

  • Default providers: Use parent providers by default.

The overridden/defaults/directory/providers 'inherit' up the config's Parent Chain.

This makes it easy to override values in some parent… perhaps in a unit-test, or while a documentation generator is running, or if some library your calling wants to use a different set of providers, etc....

If you pass a use_parent=False, no parent will be used or consulted. If anyone in the Parent Chain has use_parent==false, the parent-chain will stop there.

By xsentinels.Default: We lookup the parent by getting the current Config via current XContext; If that's ourselves, then we grab the parent context's Config resource. This lookup occurs every time we are asked for a .CONFIG_NAME to see if there is an override for it, etc. [see parent is used to find section above]. That means the Config's parent can change depending on the current context the time the .CONFIG_NAME is asked for.

defaults : Union[DirectoryListing, Dict[str, Any], xsentinels.Default]

If defaults are provided, these values will be used when Config is asked for something that does not exist anywhere else. ie: Has not been overridden [by directly setting value on Config or a parent Config], and also not in any provider.

Basically, if Config can't find a value anywhere else, it will as a last-resort check these defaults. If a value in defaults is present for the configuration name/key in question, the value in defaults will be returned. This default value is NOT cached via the DynamoCacher [in-fact, the DynamoCacher will cache the fact that the config var in question does not exist]. If the cacher reports that a particular var does not exist [reminder: the cache entries eventually expire] we skip checking the providers and just check the defaults.

See the Config class doc, and the 'Search Order' section.

environment : Dict[str, Default]

Used to easily override the APP_ENV / XconSettings.environment. Infact, __init__ will simply do this if you provide a value for environment:

self.environment = environment

Can be used when APP_ENV is needed mainly for constructing default directory paths (ie: /{APP_NAME}/{APP_ENV}/...) but not effect the cacher's hash-key at the same time.

The cacher always uses XconSettings.environment for it's dynamo table hash-key. That way whatever xcon_settings are looked up from alternet environment/service names; it won't try to write them into that apps service/enviroment cached values.

It will keep the newley cached values into the XconSettings.service and xcon.conf.XconSettings.enviroment hash-value.

It will still keep track of the values in the overriden service/enviroment via the dynamo tables range-key, so things will still be properly/sepeatly cached correctly.

service : Dict[str, Default]

Used to easily override the APP_NAME / XconSettings.service. Infact, __init__ will simply do this if you provide a value for service:

self.service = service

Can be used when APP_ENV is needed mainly for constructing default directory paths (ie: /{APP_NAME}/{APP_ENV}/...) but not effect the cacher's hash-key at the same time.

The cacher always uses XconSettings.environment for it's dynamo table hash-key. That way whatever xcon_settings are looked up from alternet environment/service names; it won't try to write them into that apps service/enviroment cached values.

It will keep the newley cached values into the XconSettings.service and xcon.conf.XconSettings.enviroment hash-value.

It will still keep track of the values in the overriden service/enviroment via the dynamo tables range-key, so things will still be properly/sepeatly cached correctly.

Expand source code
class Config(Dependency):
    """
    Lets you easily get configuration values from various sources.

    You should read [Config Class Overview](#config-class-overview) first because it's a
    high-level overview of Config. Also, read the associated [Quick Start](#quick-start) that's
    there too. What you'll find below are implementation details that go into more depth on how
    Config works in various scenarios.

    .. todo::
        At some point in the future I would like to implement __getitem__ to have the Config class
        act sort of like a dictionary. If I did that, I would like the ability to iterate
        over all the current configuration key/values that the Config object knows about.
        Doing this would be a bit involved, so for now I am leaving dict/mapping like
        access non-implemented.
    """

    # These are guaranteed to be here after __init__
    # These contain the name/value pairs for our overrides and defaults.
    _override: DirectoryListing
    _defaults: DirectoryListing

    # Set in __init__, used to know if user wants us to user parent-chain or not.
    _use_parent: bool = True

    # These are here to store info from __init__, for lazy allocation when needed;
    # and to know what the user actually wanted [ie: Default, Blank list, None, Etc].
    _cacher: Union[DefaultType, None] = Default

    # These are also from __init__ [see last comment above _cacher]
    _providers: OrderedDefaultSet[Type[Provider]]
    _directories: OrderedDefaultSet[Directory]
    _exports: OrderedDefaultSet[Directory]

    @classmethod
    def current(cls):
        """ Calls 'cls.grab()', just am alternative name for the same thing, may make things
            a bit more self-documenting, since `Config` could be used in a lot of places.

        """
        return cls.grab()

    def __init__(
            self, *,
            directories: Union[Iterable[DirectoryOrPath], DefaultType] = Default,
            providers: Union[Iterable[Type[Provider]], DefaultType] = Default,
            cacher: Union[Type[DynamoCacher], DefaultType, None] = Default,
            use_parent: bool = True,
            defaults: Union[DirectoryListing, Dict[str, Any], DefaultType] = Default,
            service: str = Default,
            environment: str = Default
    ):
        """
        Create a new Config object. Normally you would just leave everything at their Default
        values. You can change any of them if needed. If you pass a None for any parameter
        that defaults to Default, that aspect will be disabled/not used. For example, if you
        pass a None for directories/providers, no directories/providers will be searched.

        Parameters
        ---------
        directories: Union[Iterable[xcon.directory.DirectoryOrPath], xsentinels.Default]
            List of directories/paths to search when querying for a name.
            
            If `xsentinels.Default`: Uses the first one from [Parent Chain](#parent-chain).
            
            If everyone in the parent chain is set to `Default`,
            uses `xcon.conf.XconSettings.directories`.

            Various ways to change what directories to use:

            >>> from xsentinels import Default
            >>> 
            >>> with Config(directories=['/some/other/path', Default]):
            ...     # Will first look in `/some/other/path`,
            ...     # if not found next looks at default paths/directories.
            ...     assert config['some_var'] == 'expected-value'

            .. note:: This will also preserves the current service name
                As your just changing the directories used, and not the service name.
                This means the cache-path is not changed, so you don't have to add permissions
                to read/write other cache-path

            If you want to lookup the standard/default ones first, you can do this too:

            >>> my_directories = [
            ...     Default,
            ...     # Directory objects support `{environment}` and `{service}`
            ...     # placeholders in their path.
            ...     Directory(service='other_app', env='{environment}'),
            ...     Directory(service='other_app/all'),  
            ... ]
            >>> with Config(directories=my_directories):
            ...     assert config.get('some_var') == 'expected-value'

            When `Default` is resolved, after the ones your inserting your self,
            it will use the standard app service/env.

            This means it will first look for the two directiores first from other_app,
            and then if it still can't find the var it will next look at the current
            app/service for the var.

        providers: Union[Iterable[Type[xcon.provider.Provider]], xsentinels.DefaultType]
            List of provider types to use. If set to `Default`, uses the first one from
            [Parent Chain](#parent-chain). If everyone in the parent chain is set to `Default`,
            uses `xcon.conf.XconSettings.providers`.

        cacher: Type[xcon.provider.ProviderCacher]
            In the future, I may allow other cachers to be passed in via this param, but right
            now only the DynamoCacher is used and the only values you can use are:

            - If `None`:
                - No flattened high-level caching will be used. The individual
                  providers will still cache things internally per-directory/provider.

            - If left as `xsentinels.Default`:
                - Must have a service/enviroment we can use
                  (ie: APP_ENV / APP_NAME;
                      `xcon.conf.XconSettings.environment` / `xcon.conf.XconSettings.service`
                  ).
                  If so, we will attempt to read/write to a special Dynamo table that has
                  a flattened list of name/value pairs that are tied to the current service,
                  enviroment, directory-chain, provider-chain at the time the value is asked for.

            The cacher-path will use current service/environment
            (`APP_ENV` / `APP_NAME`;
            `xcon.conf.XconSettings.environment` / `xcon.conf.XconSettings.service`
            ).

            If you want change where you lookup variables without effecting the cacher-path,
            you can change the directories that Config uses.

            See below on `service` and `directories` paramters for examples/details.

        use_parent: bool
            [Parent Chain](#parent-chain) is used to find:

               - Overridden config values; these are values that set directly
                 on the Config object; ie: `xcon.config.config`.CONFIG_NAME = "Some Value to Override With"

               - Default values; these are used when config can find no other value for a
                 particular .CONFIG_NAME. See `set_default`

               - Default directories: Use parent directories by default.

               - Default providers: Use parent providers by default.

            The overridden/defaults/directory/providers 'inherit' up the config's
            [Parent Chain](#parent-chain).

            This makes it easy to override values in some parent... perhaps in a unit-test, or
            while a documentation generator is running, or if some library your calling
            wants to use a different set of providers, etc....

            If you pass a `use_parent=False`, no parent will be used or consulted. If anyone
            in the [Parent Chain](#parent-chain) has `use_parent==false`, the parent-chain
            will stop there.

            By `xsentinels.Default`:
               We lookup the parent by getting the current Config via current XContext;
               If that's ourselves, then we grab the parent context's Config resource.
               This lookup occurs every time we are asked for a .CONFIG_NAME to see if
               there is an override for it, etc. [see `parent is used to find` section above].
               That means the Config's parent can change depending on the current context the
               time the .CONFIG_NAME is asked for.

        defaults: Union[xcon.directory.DirectoryListing, Dict[str, Any], xsentinels.Default]
            If `defaults` are provided, these values will be used when Config is asked for
            something that does not exist anywhere else. ie: Has not been overridden [by directly
            setting value on Config or a parent Config], and also not in any provider.

            Basically, if Config can't find a value anywhere else, it will as a last-resort
            check these defaults. If a value in defaults is present for the configuration
            name/key in question, the value in defaults will be returned.
            This default value is NOT cached via the DynamoCacher [in-fact, the DynamoCacher will
            cache the fact that the config var in question does not exist]. If the cacher reports
            that a particular var does not exist [reminder: the cache entries eventually expire]
            we skip checking the providers and just check the defaults.

            See the Config class doc, and the 'Search Order' section.

        environment: Dict[str, Default]
            Used to easily override the `APP_ENV` / `xcon.conf.XconSettings.environment`.
            Infact, `__init__` will simply do this if you provide a value for `environment`:

            >>> self.environment = environment

            Can be used when APP_ENV is needed mainly for constructing default directory paths
            (ie: `/{APP_NAME}/{APP_ENV}/...`) but not effect the cacher's hash-key at the same
            time.
            
            The cacher always uses `xcon.conf.XconSettings.environment` for it's dynamo table
            hash-key.  That way whatever xcon_settings are looked up from alternet environment/service
            names; it won't try to write them into that apps service/enviroment cached values.
            
            It will keep the newley cached values into the `xcon.conf.XconSettings.service`
            and `xcon.conf.XconSettings.enviroment` hash-value.
            
            It will still keep track of the values in the overriden service/enviroment
            via the dynamo tables range-key, so things will still be properly/sepeatly
            cached correctly.


        service: Dict[str, Default]
            Used to easily override the `APP_NAME` / `xcon.conf.XconSettings.service`.
            Infact, `__init__` will simply do this if you provide a value for `service`:

            >>> self.service = service

            Can be used when APP_ENV is needed mainly for constructing default directory paths
            (ie: `/{APP_NAME}/{APP_ENV}/...`) but not effect the cacher's hash-key at the same
            time.
            
            The cacher always uses `xcon.conf.XconSettings.environment` for it's dynamo table
            hash-key.  That way whatever xcon_settings are looked up from alternet environment/service
            names; it won't try to write them into that apps service/enviroment cached values.
            
            It will keep the newley cached values into the `xcon.conf.XconSettings.service`
            and `xcon.conf.XconSettings.enviroment` hash-value.
            
            It will still keep track of the values in the overriden service/enviroment
            via the dynamo tables range-key, so things will still be properly/sepeatly
            cached correctly.
        """  # noqa
        super().__init__()

        self._override = DirectoryListing()
        self._defaults = DirectoryListing()

        # By default, we grab the ones from the parent chain and use them.
        self._exports: Dict[Union[DefaultType, str], None] = {Default: None}

        self._use_parent = use_parent

        # We lazy-lookup directories if it's `Default`, this is so you can directly override
        # xcon_settings.service and/or xcon_settings.environment if you want to easily change
        # the defaults.
        # See 'self.directories' property.
        self.directories = directories

        # This property will lazily be used to create self.provider_chain when the chain
        # is requested for the first time.
        self._providers = {x: None for x in xloop(providers, default_not_iterate=[str])}

        # We lazy-lookup cacher if it's Default or a Type.
        # See 'self.cacher' property.
        _check_proper_cacher_or_raise_error(cacher)
        self._cacher = cacher

        if isinstance(defaults, dict):
            for name, value in defaults.items():
                self.set_default(name, value)

        self._service = service
        self._environment = environment

    @property
    def providers(self) -> Union[DefaultType, Iterable[Union[Type[Provider], DefaultType]]]:
        """ Lets you see providers set directly on this config object.

            If set to Default, it  means we look to our [Parent Chain](#parent-chain) first,
            and if one of them don't have any set to then use sensible defaults.

            Otherwise it's a list of `xcon.provider.Provider` types and/or Default.
        """
        return self._providers.keys()

    @property
    def directories(self) -> Union[DefaultType, Iterable[Union[Directory, DefaultType]]]:
        """ Lets you see directories set directly on this config object.

            If set to Default, it  means we look to our [Parent Chain](#parent-chain) first,
            and if one of them don't have any set to then use sensible defaults.

            Otherwise it's a list of `xcon.directory.Directory` and/or Default.
        """
        return self._directories.keys()

    @directories.setter
    def directories(
            self,
            value: Union[Iterable[Union[DefaultType, DirectoryOrPath]], DefaultType]
    ):
        """ List of all directories set on self, by default it's just `[Default]`.
            This DOES NOT resolve the `Default` if it's in the list.  That's resolved
            when you ask for the `Config.directory_chain`.
        """
        # make an ordered-set out of this.
        dirs: OrderedDefaultSet = {}
        for x in xloop(value, default_not_iterate=[str]):
            if x is not Default:
                x = Directory.from_path(x)
            dirs[x] = None

        self._directories = dirs

    @providers.setter
    def providers(self, value: Union[DefaultType, Iterable[Union[DefaultType, Type[Provider]]]]):
        """ List of all providers set on self, by default it's just `[Default]`.
            This DOES NOT resolve the `Default` if it's in the list.  That's resolved
            when you ask for the `Config.provider_chain`.
        """
        # make an ordered-set out of this.
        self._providers = {x: None for x in xloop(value, default_not_iterate=[str])}

    def add_provider(self, provider: Type[Provider]):
        """ Adds a provider type to end of my provider type list [you can see what it is for
            myself via `Config.providers`].  By default, a Config object starts off with
            a provider list of just `[Default]`. By adding to the end of this, we still
            pick up the parent/default providers. This method simply appends to whatever
            we currently have.  If provider is already in list, nothing changes
            [ie: existing order will not change].
        """
        # If we already have it, no need to do anything else.
        if provider in self._providers:
            return

        # Add Provider type; using dict as an 'ordered set'; see xsentinels.OrderedSet.
        self._providers[provider] = None

    def add_directory(self, directory: Union[Directory, str, DefaultType]) -> 'Config':
        """ Adds a directory to end of my directory list [you can see what it is for
            myself via `Config.directories`].  By default, a Config object starts off with
            a directory list of just `[Default]`. By adding to the end of this, we still
            pick up the parent/default directories.  If directory is already in list,
            nothing changes [ie: existing order will not change].

            Returns self, so you can chain it.
        """
        # If we already have it, no need to do anything else.
        if directory in self._directories:
            return self

        # Add Directory; using dict as an 'ordered set'; see xsentinels.OrderedSet
        self._directories[directory] = None
        return self

    def add_export(self, *, service: str):
        """
        These are added to the `Config.directory_chain` after the normal directories from
        `Config.directories`. The purpose of these are to see 'exported' values from other
        services.  We currently use the current `xcon.conf.XconSettings.environment` or
        `Config.resolved_environment` when looking at the exported values for a service.

        Directories that are created in the `Config.directory_chain` from these exports follow
        this pattern:

        "/{service}/{environment}/export"


        By default, the export list is just this:

        ( `xsentinels.Default`, )

        When you add more exports via `Config.add_export`, it will append to the end of this list.
        The `Default` export are any exports from the parent `Config` object.
        That way we still add whatever exports are in parent and then we add the ones
        from self.

        If you want to remove the [Default] option, see `Config.set_exports`.

        .. todo:: Someday in the future, we will probably add other parameters to override
            what service to use.

        Args:
            service: Name of the service you want exported values from.
                We currently use the environment that the Config object sees. At some point in
                the future we may support also adding an explict environment here as well
                (so you don't have to use the 'current' environment name, ie: testing/prod/etc;
                you could use whatever you want).
        """
        # This is an OrderedDefaultSet, add in the service...
        self._exports[service] = None

    def set_exports(self, *, services: Iterable[Union[str, DefaultType]]):
        """
        This allows you to set all the exports. Right now
        we only support setting them by service [and not environment]. See `Config.add_export`
        for more details.

        This replaces all current services. By default, the export list is this:

        ( `xsentinels.Default`, )

        `Default` when resolved means we ask the parent chain for any exports.
        If you set the exports without including this then the parent-chain won't be consulted.

        See `Config.add_service` for a way to easily append to end of list and keeps what's
        currently in it.

        Args:
            services (Iterable[Union[str, `xsentinels.Default`]]): List of exports you want
                to add by service name. If you don't add the `xsentinels.Default` somewhere in
                this list then we will NOT check the parent-chain
        """
        self._exports = {x: None for x in xloop(services, default_not_iterate=[str])}

    def get_exports_by_service(self):
        """ List of services we currently check their export's for. This only lists the exports
            directly assigned to self (not in the [Parent Chain](#parent-chain)). Allows you to
            find out what this config object as set directly on it for which exports we
            look for per-service.
        """
        return self._exports.keys()

    def set_override(self, name, value: [Any, Default]):
        """
        Sets an override on self. When someone asks for this value, this will be returned
        regardless of what any provider or environmental variable as set.

        You can also set an override by setting a value for a config-name directly on `Config`
        via this syntax:

        >>> from xcon.config import config
        >>> config['some_override_name'] = "my override value"

        For details see [Naming Rules](#naming-rules).

        .. important:: This will also affect child config objects!
            They will look for overrides set on a parent before looking at any providers.

            For more details see [Parent Chain](#parent-chain) and [Overrides](#overrides) topics.

        Args:
            name: Name of the item to remove, case-insensitive.
            value (Union[Any, xsentinels.Default]): Can be any value. If Default is used
                we will instead call `Config.remove_override(name)` for you to remove the value.
        """
        if value is Default:
            self.remove_override(name)
            return

        # Checked for default already, from this point forward it's just `Any` type.
        value: Any
        override_item = DirectoryItem(
            directory="/_override", name=name, value=value, source=f"Config.set_override",
            cacheable=False
        )

        xlog.info(f"Setting Config override for item ({override_item}).")
        self._override.add_item(
            override_item
        )

    def get_override(self, name) -> Union[Any, DefaultType, None]:
        """
        Returns a value of override for `name` was directly set on this config object
        in one of two ways:

        - `config.set_override`
        -  `config.SOME_VAR = "a-value"`

        The returned value is `xsentinels.Default` if no override is found;
        this is so you can distinguish between overriding to None or no override set at all
        (`xsentinels.Default` evaluates to `False`, just like how `None` works).

        .. warning:: Only returns a value if overrides was directly set on self!

            (ie: **won't** consult the [Parent Chain](#parent-chain)).
            The parent chain (parent configs) are consulted when looking up a config value
            normally via `Config.get`. Overrides in self and then in parents are checked first.

            `get_override` method is here so you can examine a specific `Config` object and
            determine if there are any overrides set directly on it.

        Attributes:
            name (str): Name to use to get override [case-insensitive].

        Returns:
            Union[Any, xsentinels.Default]: The value, or `xsentinels.Default` if no value was
                set for `name`. This allows you to distinguish between overriding a value to
                `None` and no override being set in the first place
                (`xsentinels.Default` evaluates to `False`, just like how `None` works).
        """
        item = self._override.get_item(name)
        if item:
            return item.value
        return Default

    def remove_override(self, name):
        """
        Remove override **ONLY** on self.
        This will not remove overrides from a parent.

        .. warning:: This WON'T affect any override set on a parent!
            see [Parent Chain](#parent-chain).

        Someday we may make it easier to publicly go through the parents
        (right now there are internal/private methods that do this).

        If you don't like an override, you can override the override by setting an override
        on a child/current `Config` object (see `Config.set_override`).

        Probably should not mess with config objects from higher up that you don't know
        anything about in any case.  That's why I've hesitated about publicly exposing
        the parent chain too much.


        You can remove overrides in various ways, such as:

        ```python
        from xcon import config
        from xsentinels import Default

        # Alternate Method 1:
        config.SOME_NAME = Default

        # Alternate Method 2:
        config.set_override("SOME_NAME", Default)
        ```

        At the moment these ways ^ will not remove an override from a parent.

        If we do decide we want an ability  to "white-out" an override;
        I would probably do it such that you could tell a child to not check parent(s)
        overrides on a specific value
        (ie: I would set the override value to `Default` on self internally, to indicate that).
        """
        xlog.info(f"Removing Config override for name ({name}).")
        self._override.remove_item_with_name(name=name)

    @property
    def service(self) -> Union[DefaultType, str]:
        """
        Returns the value passed into __init__ for `service` or set on property `Config.service`.

        ```python
        # Ways to override service on a Config object.
        config.service = "some-service"
        config = Config(service="some-service")
        ```

        If it was not overridden, then returns `Default`. This is a placeholder that represents
        the default value should be used. You can set the service via `config.service = Default`
        to remove any overridden and return to having it lookup the service like normal
        from any parent(s) or `xcon.conf.XconSettings.service`.

        If your interested it knowing what it's current using as the service name, you could
        use `Config.resolved_service`. It will return the currently resolved service name
        that is currently being used for that config object.
        """
        return self._service

    @property
    def environment(self) -> Union[str, DefaultType]:
        """
        Returns the value passed into __init__ for `environment` or set on property
        `Config.environment`.

        ```python
        # Ways to override service on a Config object.
        config.environment = "some-env"
        config = Config(environment="some-env")
        ```

        If it was not overridden, then returns `Default`. This is a placeholder that represents
        the default value should be used. You can set the service via
        `config.environment = Default` to remove any overridden and return to having it lookup the
        service like normal from any parent(s) or `xcon.conf.XconSettings.environment`.

        If your interested it knowing what it's current using as the service name, you could
        use `Config.resolved_environment`. It will return the currently resolved environment name
        that is currently being used for that config object.
        """
        return self._environment

    @environment.setter
    def environment(self, value):
        """ See `Config.environment getter for details. """
        self._environment = value

    @service.setter
    def service(self, value):
        """ See `Config.service getter for details. """
        self._service = value

    @property
    def resolved_service(self):
        """
        Will check self (`Config.service`), the [Parent Chain](#parent-chain) and finally
        `xcon.conf.XconSettings.service` for the current value this Config object should
        use when resolving directory paths to lookup config values.
        """
        return self._service_with_cursor(cursor=self._parent_chain().start_cursor())

    @property
    def resolved_environment(self):
        """
        Will check self (`Config.environment`), the [Parent Chain](#parent-chain) and finally
        `xcon.conf.XconSettings.environment` for the current value this Config object should
        use when resolving directory paths to lookup config values.
        """
        return self._environment_with_cursor(cursor=self._parent_chain().start_cursor())

    @property
    def cacher(self) -> Optional[Union[Type[DynamoCacher], DefaultType]]:
        """ Returns what was originally passed into __init__ for `cacher`.
            It's here, so you can see how this Config object was originally configured.
        """
        return self._cacher

    @cacher.setter
    def cacher(self, value: Union[Type[DynamoCacher], DefaultType, None]):
        """ Returns what was originally passed into __init__ for `cacher`.
            It's here so you can see how this Config object was originally configured.
        """
        _check_proper_cacher_or_raise_error(value)
        self._cacher = value

    @property
    def resolved_cacher(self) -> Optional[ProviderCacher]:
        """
        Returns the cacher object that is currently being used when config values are asked
        for; at the time this is called.
        This can change later if a config object or current context is changed.

        If environmental variable CONFIG_ONLY_ENV is set to true (looked at via `os.environ` ONLY),
        we will only ever return None for the resolved cacher to use no matter what
        parent-config / self's `Config.cacher` have been set with.
        """
        return self._cacher_with_cursor(cursor=self._parent_chain().start_cursor())

    @property
    def provider_chain(self) -> ProviderChain:
        """
        `xcon.provider.ProviderChain` we are currently using.
        This is effected by what was passed into Config when it was created.
        If it was left as `xsentinels.Default`, we will get the value via the
        [Parent Chain](#parent-chain).

        See `Config` for more details.

        If environmental variable CONFIG_ONLY_ENV is set to true (looked at via `os.environ` ONLY),
        we will only have the EnvironmentalProvider in the used/returned provider chain
        no matter what parent-config / self's `Config.providers` have been set with.
        """
        return self._provider_chain_with_cursor(cursor=self._parent_chain().start_cursor())

    @property
    def directory_chain(self) -> DirectoryChain:
        """
        `xcon.directory.DirectoryChain` we are currently using.
        This is effected by what was passed into Config when it was created.
        If it was left as `xsentinels.Default`, we will get the value via the
        [Parent Chain](#parent-chain).

        See `Config` for more details.
        """
        return self._directory_chain_with_cursor(cursor=self._parent_chain().start_cursor())

    @property
    def use_parent(self) -> bool:
        """
        If `True`: we will use the [Parent Chain](#parent-chain) when looking up things such as the
        `Config.provider_chain`. as an example; if it was left as `xsentinels.Default`
        when `Config` object was created.

        If `False`: the parent will not be consulted, and anything that was not set at creation
        or added/set after creation will use the default values. See `Config` for details.

        See [Parent Chain](#parent-chain) for more details about how config-parents works.
        """
        return self._use_parent

    def set_default(self, name: str, value: Optional[Any]):
        """
        When someone tries to look up a config value [perhaps via `Config.get`] and if a value
        is not found anywhere... But someone called this to define a default for it we return
        the default value set here [or passed in via Config.__init__(defaults={...})].

        For a few examples of how this can be used, see `Config`, Search Order section.
        Also see Config.__init__(...) doc for 'defaults' param.

        Args:
            name (str): Case-insensitive name for the default config.
            value (Optional[Any]): Default value, can be anything [but are generally strings].
                If you provide None for this value param, that will be stored and will be returned
                if a default is needed for param [you can use this feature to override a
                parent-config default to None if needed].

        """
        if value is Default:
            self.remove_default(name)
            return

        default_item = DirectoryItem(
            directory="/_default/user-set", name=name, value=value, source=f"config.set_default",
            cacheable=False
        )

        xlog.info(
            "Setting Config default for item ({default_item}).",
            extra=dict(default_item=default_item)
        )
        self._defaults.add_item(default_item)

    def get_default(self, name: str) -> Optional[Any]:
        """
        Returns the default for 'name' if it was set via `Config.set_default()`. It only returns
        one if it was directly set on self. This means it **WON'T** consult
        the [Parent Chain](#parent-chain)). This is so you can more easily find/set defaults
        in the parent-chain your self if you need to track something down or some such.

        Most of the item you should just be able to use `Config.set_default` and not worry about
        an existing default set on us or some other `Config` object.

        Attributes:
            name (str): Name to use to get default [case-insensitive].

        Returns:
            Union[Any, None, xsentinels.Default]: The value, or Default if no default
                is set for name. This allows you to distinguish between defaulting a value to
                None and no default being set in the first place (`xsentinels.Default`
                looks like `False`, just like how `None` works).
        """
        item = self._defaults.get_item(name)
        if item:
            return item.value
        return Default

    def remove_default(self, name):
        """
        Remove default on self.

        .. warning:: This WON'T affect any default set on a parent,
            see [Parent Chain](#parent-chain).

        You can also call this other ways, such as:

        ```python
        from xcon import config
        from xsentinels import Default

        # Alternate Method 1:
        config.set_default("SOME_NAME", Default)
        ```
        """
        xlog.info(f"Removing Config default for name ({name}).")
        self._defaults.remove_item_with_name(name=name)

    def get(
            self,
            name: str,
            default=None,
            *,
            skip_providers: bool = False,
            skip_logging: bool = False,
            ignore_local_caches: bool = False
    ) -> Optional[str]:
        """
        Similar to dict.get(), provide name [case-insensitive] and we call `Config.get_item()`
        and return the `xcon.directory.DirectoryItem.value` of the item returned,
        or passed in `default=None` if no item was found.

        See documentation for `Config.get_item()` for more details and to find out more
        about the `skip_providers` option.

        Attributes:
            name (str): Name of the config value to lookup. Name can be of any case since
                lookup is case-insensitive. But if you can keep it all lower case it could be
                a bit more efficient, since it would not have to change it.

            default: Value to return if we don't find a config value via the normal means.

            skip_providers (bool): See self.get_items() for more details, suffice it to
                say it only returns things directly set/overridden or defaulted on self or on
                parent-chain [without consulting the providers and directories].

            skip_logging (bool): Skips logging about where we got the config value.

            ignore_local_caches (bool): allows you to ignore the local memory cache
                (as a convenience option).

                Right now it does this by resetting the entire cache for you before lookup.
                But in the future if needed, it may be more precise about what it does and may just
                retrieve that specific value from each provider until it finds the value
                (vs resetting the entire cache and bulk retrieving everything all over again).

                Mostly depends on how often we would really need to do this in the future.
                I am guessing it would be rare so the current implementation should be good enough
                for now.
        """
        if ignore_local_caches:
            InternalLocalProviderCache.grab().reset_cache()

        item = self.get_item(name=name, skip_providers=skip_providers, skip_logging=skip_logging)
        if item:
            value = item.value
            return value if value is not None else default
        return default

    def get_bool(self, name: str, default=False):
        """
        Grabs config variable for `name` and does it's best to convert it to a boolean if possible.
        If the value is:

        - None: we return `default`.
        - str: We run it though `distutils.util.strtobool` to convert it to a bool.
        - Any: Anything else, we simply call `bool(value)` on it.
        - If there is any ValueError while try to convert value, we return False.

        Args:
            name (str): Name of the config value, such as `DISABLE_DB`.
            default: If we can't find a value, what should we use? By default, it's False.

        Returns:
            bool:  We found a bool value, or you provided a boolean `default` value.
            default: This means that we could not find a config value, so return `default`;
                which defaults to False
        """
        value = self.get(name)
        if value is None:
            return default
        return bool_value(value)

    def get_value(self, *args, **kwargs) -> Optional[str]:
        """
        .. deprecated:: Deprecated in favor of using `Config.get()`.
            Right now we simply call `Config.get()` with same arguments for you and return result.
        """
        return self.get(*args, **kwargs)

    def get_item(
            self, name: str, *,
            skip_providers: bool = False,
            skip_logging: bool = False,
    ) -> Optional[DirectoryItem]:
        """
        Gets a DirectoryItem for name. If the value does not exist, we will still return a
        `xcon.directory.DirectoryItem` with a
        `xcon.directory.DirectoryItem.value` == `None`. This is because we cache the
        non-existence of items for performance reasons. This allows you to see
        where the None value came from via the `xcon.directory.DirectoryItem.directory`
        attribute.

        Attributes:
            name (str): Name to look for [will be used in a case-insensitive manner].

            skip_providers (bool): If False [default], checks all sources for the config
                values. If True, only checks for things overridden on self or a parent;
                [ie: Things directory set on self or directly on a parent Config].
                It will consult any defaults (`Config.get_default()`) if needed.

            skip_logging (bool): Skips logging about where we got the config value.

        Returns (Optional[DirectoryItem]):
            If None [only happens when skip_providers is True]; then no override/default was found.

            Otherwise, returns the item as a DirectoryItem. A DirectoryItem.value can be None.
            This means that the value is None [either it could not find it or the value
            was really set to a `None`].
        """
        # todo: Someday, use a special str subclass that will indicate that it's already in
        #       lower-case format and use that instead [therefore, we can skip lower-casing it
        #       again and again as we pass the already lower-cased name along to other methods].
        name = name.lower()

        # Otherwise, we follow standard process.
        return self._get_item(
            name=name,
            skip_providers=skip_providers,
            cursor=self._parent_chain().start_cursor(),
            skip_source_logging=skip_logging
        )

    def _providers_with_cursor(self, cursor: Optional[_ParentCursor]) -> List[Provider]:
        pass

    def _resolve_providers_with_cursor(
            self, cursor: Optional[_ParentCursor]
    ) -> OrderedSet[Type[Provider]]:
        if _env_only_is_turned_on():
            # We also disable cacher, see other place we call the `_env_only_is_turned_on` method.
            return {EnvironmentalProvider: None}

        return self._resolve_attr_values_with_cursor(
            cursor=cursor,
            attribute_name="_providers",
            # Default to xcon_settings.providers if there are any,
            # otherwise just the EnvironmentalProvider:
            defaults_factory=lambda: xcon_settings.providers or [EnvironmentalProvider]
        )

    def _resolve_directories_with_cursor(
            self,
            cursor: Optional[_ParentCursor],
            service: Union[str, DefaultType, None] = Default,
            environment: Union[str, DefaultType, None] = Default
    ) -> OrderedSet[Directory]:
        # Making it a callable to make it lazy.

        def defaults_factory():
            return self._standard_directories(
                cursor=cursor,
                service=service,
                environment=environment
            )

        directories = self._resolve_attr_values_with_cursor(
            cursor=cursor,
            attribute_name="_directories",
            defaults_factory=defaults_factory
        )

        exported = self._resolve_attr_values_with_cursor(
            cursor=cursor,
            attribute_name="_exports",
            defaults_factory=tuple  # Fast, empty iterable
        )

        if service is Default:
            service = self._service_with_cursor(cursor)
        if environment is Default:
            environment = self._environment_with_cursor(cursor)

        if exported:
            # Any new values will be added to end, nothing will happen to order of existing ones.
            for x in exported:
                directories[Directory(service=x, env=environment, is_export=True)] = None

        directories = {
            k.resolve(service=service, environment=environment): None for k in directories
        }

        return directories

    def _resolve_attr_values_with_cursor(
            self,
            cursor: Optional[_ParentCursor],
            attribute_name: str,
            defaults_factory: Callable[[], Iterable[T]]
    ) -> OrderedSet[T]:
        """ Internally, we are using a dict as an ordered-set, python 3.7 guarantees dicts
            keep their insertion order.

            This will return an ordered-dict, where the keys are the values. This function
            will resolve any 'Default' values encountered with their parent version.
        """
        default = Default

        values: OrderedDefaultSet[T] = getattr(self, attribute_name)
        if default not in values:
            # Ensure we don't accidentally modify this ordered set somewhere else.
            return copy(values)

        if cursor:
            parent_values = cursor.parent._resolve_attr_values_with_cursor(
                cursor=cursor.next_cursor(),
                attribute_name=attribute_name,
                defaults_factory=defaults_factory
            )
        else:
            parent_values = {x: None for x in defaults_factory()}

        # We have a default we need to 'insert' our parent providers into....
        # First we check to see if we only have 'Default'...

        if len(values) == 1:
            # If we only have one value, it's only value is 'Default', so easy, just return
            # what we have and move on.
            return parent_values

        # If we have more then just 'Default', we replace it with parent providers...
        final_values: OrderedSet[T] = {}
        for p in values:
            if p is default:
                final_values.update(parent_values)
            else:
                final_values[p] = None

        return final_values

    def _provider_chain_with_cursor(self, cursor: Optional[_ParentCursor]) -> ProviderChain:
        """
        I first check to see if I have/use a parent and our provider list is Default;
        if that's the case we return the parent Config's provider_chain.

        Otherwise we need to create a provider_chain and cache/return that now and in the future.
        """

        # Note from Josh: Keep things simple for now, just create provider chain when needed.
        #
        # if we find creating/finding the provider chain takes to long, then we can cache
        # the chains by provider values. We could also keep them around and reset them if
        # any of our parent's providers are changed, etc. I decided that it's probably not
        # expensive and so not to pre-optimize and just worry about it down the road if that's
        # not the case anymore.

        provider_types = self._resolve_providers_with_cursor(cursor=cursor)
        return ProviderChain(providers=provider_types)

    def _cacher_with_cursor(
            self,
            cursor: Optional[_ParentCursor]
    ) -> Optional[DynamoCacher]:
        # if user set cacher to None, they don't want caching enabled, so return None.
        cacher = self._cacher
        if cacher is None:
            return None

        # if user wants to force only the environmental provider to be used, disable cacher too.
        if _env_only_is_turned_on():
            return None

        # If we have a parent, and user wants the Default cacher, ask the parent for it.
        if cursor and cacher is Default:
            return cursor.parent._cacher_with_cursor(cursor=cursor.next_cursor())

        # If cacher is Default, right now the only supported cacher type is DynamoCacher.
        if cacher is Default:
            # We don't check self, only an environmental variable for this.
            # This is so you don't have to modify the code to disable cacher by default.
            # If you want to disable cacher via code, do this instead:
            #
            #  with Config(cacher=None):
            #      pass
            #
            # or
            #
            #  @Config(cacher=None)
            #  def some_method():
            #      pass
            #
            # BUT if someone passes `Config(cacher=DynamoCacher)` explicitly we will use that
            # regardless of what `XCON_DISABLE_DEFAULT_CACHER` is set too.
            #
            # Lower-casing it because `EnvironmentalProvider` will do that for us when looking it
            # up (it looks it up in a case-insensitive manner).
            # Trying to make it a tiny bit more efficient since this is called a lot.
            env_provider = EnvironmentalProvider.grab()
            if xcon_settings.disable_default_cacher:
                return None
            cacher = DynamoCacher

        # We only accept types at this point [we already handled None and Default cases above ^],
        # the idea is to use it as a resource, so that we can have multiple config objects
        # use the same 'cacher' type.  Right now, only DynamoCacher is even supported,
        # this is more of a sanity check. If we ever have other ProviderCacher subclasses in
        # the future, we can update this to be more open [but should check for inspect.isclass
        # in that future, we want only class types from the user at this point].
        if cacher is not DynamoCacher:
            raise ConfigError(
                f"Trying to get the cacher, but the type the user wants to use is not a "
                f"DynamoCacher type: ({cacher})  In the future I may support other cacher "
                f"types; but right now we only support either None, Default or "
                f"xcon.providers.dynamo.DynamoCacher."
            )

        # Grab the current cacher resource, it's a ProviderCacher type of some sort and
        # so is a xinject.dependency.Dependency
        # (right now, cacher can only be a DynamoCacher type;
        # although we can change that in the future if we decide to change how caching works)
        return cacher.grab()

    def _resolve_attr_with_cursor(
            self,
            cursor: Optional[_ParentCursor],
            attribute_name: str,
            defaults_factory: Callable[[], T]
    ) -> T:
        # if user set cacher to None, they don't want caching enabled, so return None.
        value = getattr(self, attribute_name)
        if value is not Default:
            return value

        if not cursor:
            # We don't have any more parents to check, get the default value via factory callable.
            return defaults_factory()

        # If we have a parent via the next-cursor, use that to see if we can find a value.
        return cursor.parent._resolve_attr_with_cursor(
            cursor=cursor.next_cursor(),
            attribute_name=attribute_name,
            defaults_factory=defaults_factory
        )

    def _get_item(
            self, name: str, *,
            skip_providers: bool = False,
            skip_defaults: bool = False,
            cursor: Optional[_ParentCursor],
            skip_source_logging: bool = False,
    ) -> Optional[DirectoryItem]:

        item = None
        provider_chain = None
        directory_chain = None
        try:
            item = self._override.get_item(name)
            if not item and cursor:
                item = cursor.parent._get_item(
                    name=name,
                    skip_providers=True,
                    skip_defaults=True,
                    cursor=cursor.next_cursor(),
                    skip_source_logging=skip_source_logging
                )

            if item:
                return item

            # Check to see if we are skipping providers, this happens when we only want to look
            # at overrides and/or default values to fulfill the request. This mostly only happens
            # for 'app_env' and 'service_name' config vars.
            if not skip_providers:
                # We skip logging about this, since we normally don't care... Just a lot of extra
                # useless log messages about where we keep getting the service or environment from.
                service = self._service_with_cursor(cursor)
                environment = self._environment_with_cursor(cursor)

                cacher = None

                # We will disable caching if we don't have a defined service (ie: 'global' service)
                if not service or service != "global":
                    cacher = self._cacher_with_cursor(cursor=cursor)

                directory_chain = self._directory_chain_with_cursor(
                    cursor=cursor,
                    service=service,
                    environment=environment
                )
                provider_chain = self._provider_chain_with_cursor(cursor=cursor)

                cache_dir = None
                use_cacher = bool(cacher and provider_chain.have_any_cachable_providers)
                if use_cacher:
                    # todo: Consider passing this into _directory_chain_with_cursor instead
                    #   of the individual components.
                    cache_dir = Directory.from_components(service=service, environment=environment)
                else:
                    cacher = None

                item = provider_chain.get_item(
                    name=name,
                    directory_chain=directory_chain,
                    cacher=cacher,
                    environ=cache_dir
                )

            if skip_defaults:
                # If they skip defaults then return None...
                # with skip_defaults == False: We return a DirectoryItem with None as the value.
                # todo: Double check to see if we want to return item vs None
                #   [adjust comment just above to reflect decision].
                return item

            if not item or (item.value is None and item.directory.is_non_existent):
                # Check for default values next...
                default_item = self._get_default_item_with_cursor(name=name, cursor=cursor)
                if default_item:
                    item = default_item
        finally:
            # We normally only want to log about things the users actually requests,
            # and not things used to fulfill the request.
            if not skip_source_logging:
                self._log_about_item_retrieval(name, item, directory_chain, provider_chain)

        return item

    # noinspection PyMethodMayBeStatic
    def _log_about_item_retrieval(
        self,
        original_name: str,
        item: DirectoryItem,
        directory_chain: DirectoryChain = None,
        provider_chain: ProviderChain = None
    ):
        env_only_enabled = _env_only_is_turned_on()

        if item and not item.directory.is_non_existent:
            # FYI: What's nice about doing it this way are these string formatting placeholders
            # will only evaluated if log message is actually emitted.
            # This logging message could be called a lot.
            xlog.debug(
                "Config found ({config_var_name}); returned {config_item}; extra metadata {meta}; "
                f"env_only_enabled({env_only_enabled}).",
                extra=dict(
                    config_var_name=original_name,
                    config_item=item,
                    meta=item.supplemental_metadata,
                )
            )
        elif item:
            directories = directory_chain.directories if directory_chain else []
            providers = provider_chain.providers if provider_chain else []

            supplemental_msg = ""
            if item.from_cacher:
                supplemental_msg = f"non-existence entry was found in cacher; "

            xlog.debug(
                "Config not found ({config_var_name}); "
                f"{supplemental_msg}"
                "in directories ({directories}), "
                "for providers ({providers}); extra metadata ({meta}); "
                f"env_only_enabled({env_only_enabled}).",
                extra=dict(
                    config_var_name=original_name,
                    directories=[d.path for d in directories],
                    providers=[p.name for p in providers],
                    source=item.source,
                    meta=item.supplemental_metadata,
                )
            )

    def _get_default_item_with_cursor(
            self, name: str, cursor: Optional[_ParentCursor]
    ) -> Optional[DirectoryItem]:
        item = self._defaults.get_item(name=name)
        if item:
            return item

        if cursor:
            return cursor.parent._get_default_item_with_cursor(
                name=name,
                cursor=cursor.next_cursor()
            )

        return None

    def _parent_chain(self) -> _ParentChain:
        """
            See [Parent Chain](#parent-chain).

            There is a concept of a parent-chain with Config if the Config object has
            their `use_parent == True` [it defaults to True]. We use the current XContext to
            construct this parent-chain. See [parent-chain].

            The parent-chain starts with the config resource in the current XContext.
            If that context has a parent context, we next grab the Config resource from
            that parent context and check it's `Config.use_parent`. If True we keep doing
            this until we reach a XContext without a parent or a `Config.use_parent` that is False.

            We take out of the chain any config object that is myself. The only objects in
            the chain are other Config object instances.

            The parent chain is generally consulted when we encounter a `Default` value.
            If when reaching the last parent in the chain, we still have a `Default` value,
            sensible/default values are constructed [if they have not already been] and used.
        """
        use_parent = self._use_parent
        found_self = False
        skip_adding_more_parents = False

        chain = []
        for config_resource in XContext.grab().dependency_chain(Config, create=True):
            if config_resource is self:
                found_self = True
                if not use_parent:
                    break
                continue

            if skip_adding_more_parents:
                continue

            chain.append(config_resource)
            if not config_resource.use_parent:
                skip_adding_more_parents = True
                if use_parent:
                    # We don't need to go any further, we want to use parents and
                    # we found a parent that does not, just return what we have.
                    break
                # We keep going to see if we can find our selves, but we don't add
                # anymore to the chain. If we can't find our selves as a resource
                # in the current context parent-chain, we will return a blank chain.
                continue

        # If we did not find self and we don't want to use a parent, we return a blank parent
        # chain. This is because we are not in the current config context hierarchy, and so are
        # a 'separate' island and are cut-off from all other config objects.
        if not use_parent and not found_self:
            return _BlankParentChain

        return _ParentChain(parents=tuple(chain))

    def _directory_chain_with_cursor(
            self,
            cursor: Optional[_ParentCursor],
            service: Union[str, DefaultType, None] = Default,
            environment: Union[str, DefaultType, None] = Default,
    ) -> Optional[DirectoryChain]:
        directories = self._resolve_directories_with_cursor(
            cursor=cursor,
            service=service,
            environment=environment
        )

        return DirectoryChain(directories=directories)

    def _standard_directories(
            self,
            cursor: Optional[_ParentCursor],
            service: Union[str, DefaultType, None] = Default,
            environment: Union[str, DefaultType, None] = Default
    ) -> OrderedSet[Directory]:
        if service is Default:
            service = self._service_with_cursor(cursor)
        if environment is Default:
            environment = self._environment_with_cursor(cursor)

        return {
            d.resolve(service=service, environment=environment): None
            for d in xloop(xcon_settings.directories, default_not_iterate=[str])
        }

    def _service_with_cursor(self, cursor: Optional[_ParentCursor]) -> str:
        return self._resolve_attr_with_cursor(
            cursor=cursor,
            attribute_name='_service',
            defaults_factory=lambda: xcon_settings.service or 'global'
        )

    def _environment_with_cursor(self, cursor: Optional[_ParentCursor]) -> str:
        return self._resolve_attr_with_cursor(
            cursor=cursor,
            attribute_name='_environment',
            defaults_factory=lambda: xcon_settings.environment or 'all'
        )

    def _get_special_non_provider_item_with_cursor(
            self,
            name: str,
            hard_default: str,
            cursor: Optional[_ParentCursor],
            *,
            skip_source_logging: bool = False,
            as_item: bool = False
    ) -> Union[DirectoryItem, str]:
        """
        Returns an item/value by searching for 'name' without using providers.
        If we can't find a value, or the value we find is false we use the provided hard_default.

        Args:
            name: Name to look for [case-insensitive]
            hard_default: Default to use if we can't find a non-false like value.
            cursor: Current cursor we are using, see `_ParentCursor` for more details.
            skip_source_logging: Don't log where we got this from
                (if this is used to get something else, limits excessive logging].
            as_item: Return `DirectoryItem` if true, else we return just the value.
        """
        # We need to use internal method to preserve the cursor.
        item = self._get_item(
            name,
            skip_providers=True,
            cursor=cursor,
            skip_source_logging=skip_source_logging
        )

        if item and item.value:
            if as_item:
                return item
            return item.value

        item = EnvironmentalProvider.grab().get_item_without_environ(name)
        if not item or not item.value:
            item = DirectoryItem(
                directory="/_default/hard-coded",
                name=name,
                value=hard_default,
                source=f"config.hard-coded-default={hard_default}",
                cacheable=False
            )

        return item if as_item else item.value

    def __getitem__(self, key):
        """ Allows config to be used like a dictionary, in that you can look up config values
            via the `config['some_key']` syntax.
        """
        return self.get(key)

    def __setitem__(self, key, value):
        """
        Allows one to override values on config when used like a dictionary,
        in that you can set override values on config values via the
        `config['some_key'] = 'override-with-value'` syntax.

        To remove an override you can either set the value to `Default`;
         ie: `config['some_key'] = Default`.

        Or call `Config.remove_override`; ie: `config.remove_override('some_key')`.
        """
        self.set_override(key, value)

Ancestors

Static methods

def __init_subclass__(thread_sharable=Default, attributes_to_skip_while_copying: Optional[Iterable[str]] = Default, **kwargs)

Inherited from: Dependency.__init_subclass__

Args

thread_sharable
If False: While a dependency is lazily auto-created, we will ensure we do it per-thread, and not make it visible …
def current()

Calls 'cls.grab()', just am alternative name for the same thing, may make things a bit more self-documenting, since Config could be used in a lot of places.

Expand source code
@classmethod
def current(cls):
    """ Calls 'cls.grab()', just am alternative name for the same thing, may make things
        a bit more self-documenting, since `Config` could be used in a lot of places.

    """
    return cls.grab()
def grab() ‑> ~T

Inherited from: Dependency.grab

Gets a potentially shared dependency from the current udpend.context.XContext

def proxy() ‑> ~R

Inherited from: Dependency.proxy

Returns a proxy-object, that when and attribute is asked for, it will proxy it to the current object of cls

def proxy_attribute(attribute_name: str) ‑> Any

Inherited from: Dependency.proxy_attribute

Returns a proxy-object, that when and attribute is asked for, it will proxy it to the current attribute value on the current object of cls

Instance variables

var cacher : Union[Type[DynamoCacher], DefaultType, ForwardRef(None)]

Returns what was originally passed into init for cacher. It's here, so you can see how this Config object was originally configured.

Expand source code
@property
def cacher(self) -> Optional[Union[Type[DynamoCacher], DefaultType]]:
    """ Returns what was originally passed into __init__ for `cacher`.
        It's here, so you can see how this Config object was originally configured.
    """
    return self._cacher
var directories : Union[DefaultType, Iterable[Union[DirectoryDefaultType]]]

Lets you see directories set directly on this config object.

If set to Default, it means we look to our Parent Chain first, and if one of them don't have any set to then use sensible defaults.

Otherwise it's a list of Directory and/or Default.

Expand source code
@property
def directories(self) -> Union[DefaultType, Iterable[Union[Directory, DefaultType]]]:
    """ Lets you see directories set directly on this config object.

        If set to Default, it  means we look to our [Parent Chain](#parent-chain) first,
        and if one of them don't have any set to then use sensible defaults.

        Otherwise it's a list of `xcon.directory.Directory` and/or Default.
    """
    return self._directories.keys()
var directory_chainDirectoryChain

DirectoryChain we are currently using. This is effected by what was passed into Config when it was created. If it was left as xsentinels.Default, we will get the value via the Parent Chain.

See Config for more details.

Expand source code
@property
def directory_chain(self) -> DirectoryChain:
    """
    `xcon.directory.DirectoryChain` we are currently using.
    This is effected by what was passed into Config when it was created.
    If it was left as `xsentinels.Default`, we will get the value via the
    [Parent Chain](#parent-chain).

    See `Config` for more details.
    """
    return self._directory_chain_with_cursor(cursor=self._parent_chain().start_cursor())
var environment : Union[str, DefaultType]

Returns the value passed into init for environment or set on property Config.environment.

# Ways to override service on a Config object.
config.environment = "some-env"
config = Config(environment="some-env")

If it was not overridden, then returns Default. This is a placeholder that represents the default value should be used. You can set the service via config.environment = Default to remove any overridden and return to having it lookup the service like normal from any parent(s) or XconSettings.environment.

If your interested it knowing what it's current using as the service name, you could use Config.resolved_environment. It will return the currently resolved environment name that is currently being used for that config object.

Expand source code
@property
def environment(self) -> Union[str, DefaultType]:
    """
    Returns the value passed into __init__ for `environment` or set on property
    `Config.environment`.

    ```python
    # Ways to override service on a Config object.
    config.environment = "some-env"
    config = Config(environment="some-env")
    ```

    If it was not overridden, then returns `Default`. This is a placeholder that represents
    the default value should be used. You can set the service via
    `config.environment = Default` to remove any overridden and return to having it lookup the
    service like normal from any parent(s) or `xcon.conf.XconSettings.environment`.

    If your interested it knowing what it's current using as the service name, you could
    use `Config.resolved_environment`. It will return the currently resolved environment name
    that is currently being used for that config object.
    """
    return self._environment
var obj : Self

Inherited from: Dependency.obj

class property/attribute that will return the current dependency for the subclass it's asked on by calling Dependency.grab, passing no extra …

var provider_chainProviderChain

ProviderChain we are currently using. This is effected by what was passed into Config when it was created. If it was left as xsentinels.Default, we will get the value via the Parent Chain.

See Config for more details.

If environmental variable CONFIG_ONLY_ENV is set to true (looked at via os.environ ONLY), we will only have the EnvironmentalProvider in the used/returned provider chain no matter what parent-config / self's Config.providers have been set with.

Expand source code
@property
def provider_chain(self) -> ProviderChain:
    """
    `xcon.provider.ProviderChain` we are currently using.
    This is effected by what was passed into Config when it was created.
    If it was left as `xsentinels.Default`, we will get the value via the
    [Parent Chain](#parent-chain).

    See `Config` for more details.

    If environmental variable CONFIG_ONLY_ENV is set to true (looked at via `os.environ` ONLY),
    we will only have the EnvironmentalProvider in the used/returned provider chain
    no matter what parent-config / self's `Config.providers` have been set with.
    """
    return self._provider_chain_with_cursor(cursor=self._parent_chain().start_cursor())
var providers : Union[DefaultType, Iterable[Union[Type[Provider], DefaultType]]]

Lets you see providers set directly on this config object.

If set to Default, it means we look to our Parent Chain first, and if one of them don't have any set to then use sensible defaults.

Otherwise it's a list of Provider types and/or Default.

Expand source code
@property
def providers(self) -> Union[DefaultType, Iterable[Union[Type[Provider], DefaultType]]]:
    """ Lets you see providers set directly on this config object.

        If set to Default, it  means we look to our [Parent Chain](#parent-chain) first,
        and if one of them don't have any set to then use sensible defaults.

        Otherwise it's a list of `xcon.provider.Provider` types and/or Default.
    """
    return self._providers.keys()
var resolved_cacher : Optional[ProviderCacher]

Returns the cacher object that is currently being used when config values are asked for; at the time this is called. This can change later if a config object or current context is changed.

If environmental variable CONFIG_ONLY_ENV is set to true (looked at via os.environ ONLY), we will only ever return None for the resolved cacher to use no matter what parent-config / self's Config.cacher have been set with.

Expand source code
@property
def resolved_cacher(self) -> Optional[ProviderCacher]:
    """
    Returns the cacher object that is currently being used when config values are asked
    for; at the time this is called.
    This can change later if a config object or current context is changed.

    If environmental variable CONFIG_ONLY_ENV is set to true (looked at via `os.environ` ONLY),
    we will only ever return None for the resolved cacher to use no matter what
    parent-config / self's `Config.cacher` have been set with.
    """
    return self._cacher_with_cursor(cursor=self._parent_chain().start_cursor())
var resolved_environment

Will check self (Config.environment), the Parent Chain and finally XconSettings.environment for the current value this Config object should use when resolving directory paths to lookup config values.

Expand source code
@property
def resolved_environment(self):
    """
    Will check self (`Config.environment`), the [Parent Chain](#parent-chain) and finally
    `xcon.conf.XconSettings.environment` for the current value this Config object should
    use when resolving directory paths to lookup config values.
    """
    return self._environment_with_cursor(cursor=self._parent_chain().start_cursor())
var resolved_service

Will check self (Config.service), the Parent Chain and finally XconSettings.service for the current value this Config object should use when resolving directory paths to lookup config values.

Expand source code
@property
def resolved_service(self):
    """
    Will check self (`Config.service`), the [Parent Chain](#parent-chain) and finally
    `xcon.conf.XconSettings.service` for the current value this Config object should
    use when resolving directory paths to lookup config values.
    """
    return self._service_with_cursor(cursor=self._parent_chain().start_cursor())
var service : Union[str, DefaultType]

Returns the value passed into init for service or set on property Config.service.

# Ways to override service on a Config object.
config.service = "some-service"
config = Config(service="some-service")

If it was not overridden, then returns Default. This is a placeholder that represents the default value should be used. You can set the service via config.service = Default to remove any overridden and return to having it lookup the service like normal from any parent(s) or XconSettings.service.

If your interested it knowing what it's current using as the service name, you could use Config.resolved_service. It will return the currently resolved service name that is currently being used for that config object.

Expand source code
@property
def service(self) -> Union[DefaultType, str]:
    """
    Returns the value passed into __init__ for `service` or set on property `Config.service`.

    ```python
    # Ways to override service on a Config object.
    config.service = "some-service"
    config = Config(service="some-service")
    ```

    If it was not overridden, then returns `Default`. This is a placeholder that represents
    the default value should be used. You can set the service via `config.service = Default`
    to remove any overridden and return to having it lookup the service like normal
    from any parent(s) or `xcon.conf.XconSettings.service`.

    If your interested it knowing what it's current using as the service name, you could
    use `Config.resolved_service`. It will return the currently resolved service name
    that is currently being used for that config object.
    """
    return self._service
var use_parent : bool

If True: we will use the Parent Chain when looking up things such as the Config.provider_chain. as an example; if it was left as xsentinels.Default when Config object was created.

If False: the parent will not be consulted, and anything that was not set at creation or added/set after creation will use the default values. See Config for details.

See Parent Chain for more details about how config-parents works.

Expand source code
@property
def use_parent(self) -> bool:
    """
    If `True`: we will use the [Parent Chain](#parent-chain) when looking up things such as the
    `Config.provider_chain`. as an example; if it was left as `xsentinels.Default`
    when `Config` object was created.

    If `False`: the parent will not be consulted, and anything that was not set at creation
    or added/set after creation will use the default values. See `Config` for details.

    See [Parent Chain](#parent-chain) for more details about how config-parents works.
    """
    return self._use_parent

Methods

def __call__(self, func)

Inherited from: Dependency.__call__

This makes Resource subclasses have an ability to be used as function decorators by default unless this method is overriden to provide some other …

def __copy__(self)

Inherited from: Dependency.__copy__

Basic shallow copy protection (I am wondering if I should just remove this default copy code) …

def add_directory(self, directory: Union[Directory, str, DefaultType]) ‑> Config

Adds a directory to end of my directory list [you can see what it is for myself via Config.directories]. By default, a Config object starts off with a directory list of just [Default]. By adding to the end of this, we still pick up the parent/default directories. If directory is already in list, nothing changes [ie: existing order will not change].

Returns self, so you can chain it.

Expand source code
def add_directory(self, directory: Union[Directory, str, DefaultType]) -> 'Config':
    """ Adds a directory to end of my directory list [you can see what it is for
        myself via `Config.directories`].  By default, a Config object starts off with
        a directory list of just `[Default]`. By adding to the end of this, we still
        pick up the parent/default directories.  If directory is already in list,
        nothing changes [ie: existing order will not change].

        Returns self, so you can chain it.
    """
    # If we already have it, no need to do anything else.
    if directory in self._directories:
        return self

    # Add Directory; using dict as an 'ordered set'; see xsentinels.OrderedSet
    self._directories[directory] = None
    return self
def add_export(self, *, service: str)

These are added to the Config.directory_chain after the normal directories from Config.directories. The purpose of these are to see 'exported' values from other services. We currently use the current XconSettings.environment or Config.resolved_environment when looking at the exported values for a service.

Directories that are created in the Config.directory_chain from these exports follow this pattern:

"/{service}/{environment}/export"

By default, the export list is just this:

( xsentinels.Default, )

When you add more exports via Config.add_export(), it will append to the end of this list. The Default export are any exports from the parent Config object. That way we still add whatever exports are in parent and then we add the ones from self.

If you want to remove the [Default] option, see Config.set_exports().

TODO

Someday in the future, we will probably add other parameters to override what service to use.

Args

service
Name of the service you want exported values from. We currently use the environment that the Config object sees. At some point in the future we may support also adding an explict environment here as well (so you don't have to use the 'current' environment name, ie: testing/prod/etc; you could use whatever you want).
Expand source code
def add_export(self, *, service: str):
    """
    These are added to the `Config.directory_chain` after the normal directories from
    `Config.directories`. The purpose of these are to see 'exported' values from other
    services.  We currently use the current `xcon.conf.XconSettings.environment` or
    `Config.resolved_environment` when looking at the exported values for a service.

    Directories that are created in the `Config.directory_chain` from these exports follow
    this pattern:

    "/{service}/{environment}/export"


    By default, the export list is just this:

    ( `xsentinels.Default`, )

    When you add more exports via `Config.add_export`, it will append to the end of this list.
    The `Default` export are any exports from the parent `Config` object.
    That way we still add whatever exports are in parent and then we add the ones
    from self.

    If you want to remove the [Default] option, see `Config.set_exports`.

    .. todo:: Someday in the future, we will probably add other parameters to override
        what service to use.

    Args:
        service: Name of the service you want exported values from.
            We currently use the environment that the Config object sees. At some point in
            the future we may support also adding an explict environment here as well
            (so you don't have to use the 'current' environment name, ie: testing/prod/etc;
            you could use whatever you want).
    """
    # This is an OrderedDefaultSet, add in the service...
    self._exports[service] = None
def add_provider(self, provider: Type[Provider])

Adds a provider type to end of my provider type list [you can see what it is for myself via Config.providers]. By default, a Config object starts off with a provider list of just [Default]. By adding to the end of this, we still pick up the parent/default providers. This method simply appends to whatever we currently have. If provider is already in list, nothing changes [ie: existing order will not change].

Expand source code
def add_provider(self, provider: Type[Provider]):
    """ Adds a provider type to end of my provider type list [you can see what it is for
        myself via `Config.providers`].  By default, a Config object starts off with
        a provider list of just `[Default]`. By adding to the end of this, we still
        pick up the parent/default providers. This method simply appends to whatever
        we currently have.  If provider is already in list, nothing changes
        [ie: existing order will not change].
    """
    # If we already have it, no need to do anything else.
    if provider in self._providers:
        return

    # Add Provider type; using dict as an 'ordered set'; see xsentinels.OrderedSet.
    self._providers[provider] = None
def get(self, name: str, default=None, *, skip_providers: bool = False, skip_logging: bool = False, ignore_local_caches: bool = False) ‑> Optional[str]

Similar to dict.get(), provide name [case-insensitive] and we call Config.get_item() and return the DirectoryItem.value of the item returned, or passed in default=None if no item was found.

See documentation for Config.get_item() for more details and to find out more about the skip_providers option.

Attributes

name : str
Name of the config value to lookup. Name can be of any case since lookup is case-insensitive. But if you can keep it all lower case it could be a bit more efficient, since it would not have to change it.
default
Value to return if we don't find a config value via the normal means.
skip_providers : bool
See self.get_items() for more details, suffice it to say it only returns things directly set/overridden or defaulted on self or on parent-chain [without consulting the providers and directories].
skip_logging : bool
Skips logging about where we got the config value.
ignore_local_caches : bool

allows you to ignore the local memory cache (as a convenience option).

Right now it does this by resetting the entire cache for you before lookup. But in the future if needed, it may be more precise about what it does and may just retrieve that specific value from each provider until it finds the value (vs resetting the entire cache and bulk retrieving everything all over again).

Mostly depends on how often we would really need to do this in the future. I am guessing it would be rare so the current implementation should be good enough for now.

Expand source code
def get(
        self,
        name: str,
        default=None,
        *,
        skip_providers: bool = False,
        skip_logging: bool = False,
        ignore_local_caches: bool = False
) -> Optional[str]:
    """
    Similar to dict.get(), provide name [case-insensitive] and we call `Config.get_item()`
    and return the `xcon.directory.DirectoryItem.value` of the item returned,
    or passed in `default=None` if no item was found.

    See documentation for `Config.get_item()` for more details and to find out more
    about the `skip_providers` option.

    Attributes:
        name (str): Name of the config value to lookup. Name can be of any case since
            lookup is case-insensitive. But if you can keep it all lower case it could be
            a bit more efficient, since it would not have to change it.

        default: Value to return if we don't find a config value via the normal means.

        skip_providers (bool): See self.get_items() for more details, suffice it to
            say it only returns things directly set/overridden or defaulted on self or on
            parent-chain [without consulting the providers and directories].

        skip_logging (bool): Skips logging about where we got the config value.

        ignore_local_caches (bool): allows you to ignore the local memory cache
            (as a convenience option).

            Right now it does this by resetting the entire cache for you before lookup.
            But in the future if needed, it may be more precise about what it does and may just
            retrieve that specific value from each provider until it finds the value
            (vs resetting the entire cache and bulk retrieving everything all over again).

            Mostly depends on how often we would really need to do this in the future.
            I am guessing it would be rare so the current implementation should be good enough
            for now.
    """
    if ignore_local_caches:
        InternalLocalProviderCache.grab().reset_cache()

    item = self.get_item(name=name, skip_providers=skip_providers, skip_logging=skip_logging)
    if item:
        value = item.value
        return value if value is not None else default
    return default
def get_bool(self, name: str, default=False)

Grabs config variable for name and does it's best to convert it to a boolean if possible. If the value is:

  • None: we return default.
  • str: We run it though distutils.util.strtobool to convert it to a bool.
  • Any: Anything else, we simply call bool(value) on it.
  • If there is any ValueError while try to convert value, we return False.

Args

name : str
Name of the config value, such as DISABLE_DB.
default
If we can't find a value, what should we use? By default, it's False.

Returns

bool
We found a bool value, or you provided a boolean default value.
default
This means that we could not find a config value, so return default; which defaults to False
Expand source code
def get_bool(self, name: str, default=False):
    """
    Grabs config variable for `name` and does it's best to convert it to a boolean if possible.
    If the value is:

    - None: we return `default`.
    - str: We run it though `distutils.util.strtobool` to convert it to a bool.
    - Any: Anything else, we simply call `bool(value)` on it.
    - If there is any ValueError while try to convert value, we return False.

    Args:
        name (str): Name of the config value, such as `DISABLE_DB`.
        default: If we can't find a value, what should we use? By default, it's False.

    Returns:
        bool:  We found a bool value, or you provided a boolean `default` value.
        default: This means that we could not find a config value, so return `default`;
            which defaults to False
    """
    value = self.get(name)
    if value is None:
        return default
    return bool_value(value)
def get_default(self, name: str) ‑> Optional[Any]

Returns the default for 'name' if it was set via Config.set_default(). It only returns one if it was directly set on self. This means it WON'T consult the Parent Chain). This is so you can more easily find/set defaults in the parent-chain your self if you need to track something down or some such.

Most of the item you should just be able to use Config.set_default() and not worry about an existing default set on us or some other Config object.

Attributes

name : str
Name to use to get default [case-insensitive].

Returns

Union[Any, None, xsentinels.Default]
The value, or Default if no default is set for name. This allows you to distinguish between defaulting a value to None and no default being set in the first place (xsentinels.Default looks like False, just like how None works).
Expand source code
def get_default(self, name: str) -> Optional[Any]:
    """
    Returns the default for 'name' if it was set via `Config.set_default()`. It only returns
    one if it was directly set on self. This means it **WON'T** consult
    the [Parent Chain](#parent-chain)). This is so you can more easily find/set defaults
    in the parent-chain your self if you need to track something down or some such.

    Most of the item you should just be able to use `Config.set_default` and not worry about
    an existing default set on us or some other `Config` object.

    Attributes:
        name (str): Name to use to get default [case-insensitive].

    Returns:
        Union[Any, None, xsentinels.Default]: The value, or Default if no default
            is set for name. This allows you to distinguish between defaulting a value to
            None and no default being set in the first place (`xsentinels.Default`
            looks like `False`, just like how `None` works).
    """
    item = self._defaults.get_item(name)
    if item:
        return item.value
    return Default
def get_exports_by_service(self)

List of services we currently check their export's for. This only lists the exports directly assigned to self (not in the Parent Chain). Allows you to find out what this config object as set directly on it for which exports we look for per-service.

Expand source code
def get_exports_by_service(self):
    """ List of services we currently check their export's for. This only lists the exports
        directly assigned to self (not in the [Parent Chain](#parent-chain)). Allows you to
        find out what this config object as set directly on it for which exports we
        look for per-service.
    """
    return self._exports.keys()
def get_item(self, name: str, *, skip_providers: bool = False, skip_logging: bool = False) ‑> Optional[DirectoryItem]

Gets a DirectoryItem for name. If the value does not exist, we will still return a DirectoryItem with a DirectoryItem.value == None. This is because we cache the non-existence of items for performance reasons. This allows you to see where the None value came from via the DirectoryItem.directory attribute.

Attributes

name : str
Name to look for [will be used in a case-insensitive manner].
skip_providers : bool
If False [default], checks all sources for the config values. If True, only checks for things overridden on self or a parent; [ie: Things directory set on self or directly on a parent Config]. It will consult any defaults (Config.get_default()) if needed.
skip_logging : bool
Skips logging about where we got the config value.

Returns (Optional[DirectoryItem]): If None [only happens when skip_providers is True]; then no override/default was found.

Otherwise, returns the item as a DirectoryItem. A DirectoryItem.value can be None.
This means that the value is None [either it could not find it or the value
was really set to a `None`].
Expand source code
def get_item(
        self, name: str, *,
        skip_providers: bool = False,
        skip_logging: bool = False,
) -> Optional[DirectoryItem]:
    """
    Gets a DirectoryItem for name. If the value does not exist, we will still return a
    `xcon.directory.DirectoryItem` with a
    `xcon.directory.DirectoryItem.value` == `None`. This is because we cache the
    non-existence of items for performance reasons. This allows you to see
    where the None value came from via the `xcon.directory.DirectoryItem.directory`
    attribute.

    Attributes:
        name (str): Name to look for [will be used in a case-insensitive manner].

        skip_providers (bool): If False [default], checks all sources for the config
            values. If True, only checks for things overridden on self or a parent;
            [ie: Things directory set on self or directly on a parent Config].
            It will consult any defaults (`Config.get_default()`) if needed.

        skip_logging (bool): Skips logging about where we got the config value.

    Returns (Optional[DirectoryItem]):
        If None [only happens when skip_providers is True]; then no override/default was found.

        Otherwise, returns the item as a DirectoryItem. A DirectoryItem.value can be None.
        This means that the value is None [either it could not find it or the value
        was really set to a `None`].
    """
    # todo: Someday, use a special str subclass that will indicate that it's already in
    #       lower-case format and use that instead [therefore, we can skip lower-casing it
    #       again and again as we pass the already lower-cased name along to other methods].
    name = name.lower()

    # Otherwise, we follow standard process.
    return self._get_item(
        name=name,
        skip_providers=skip_providers,
        cursor=self._parent_chain().start_cursor(),
        skip_source_logging=skip_logging
    )
def get_override(self, name) ‑> Union[Any, DefaultType, ForwardRef(None)]

Returns a value of override for name was directly set on this config object in one of two ways:

  • config.set_override
  • config.SOME_VAR = "a-value"

The returned value is xsentinels.Default if no override is found; this is so you can distinguish between overriding to None or no override set at all (xsentinels.Default evaluates to False, just like how None works).

Warning: Only returns a value if overrides was directly set on self!

(ie: won't consult the Parent Chain). The parent chain (parent configs) are consulted when looking up a config value normally via Config.get(). Overrides in self and then in parents are checked first.

get_override method is here so you can examine a specific Config object and determine if there are any overrides set directly on it.

Attributes

name : str
Name to use to get override [case-insensitive].

Returns

Union[Any, xsentinels.Default]
The value, or xsentinels.Default if no value was set for name. This allows you to distinguish between overriding a value to None and no override being set in the first place (xsentinels.Default evaluates to False, just like how None works).
Expand source code
def get_override(self, name) -> Union[Any, DefaultType, None]:
    """
    Returns a value of override for `name` was directly set on this config object
    in one of two ways:

    - `config.set_override`
    -  `config.SOME_VAR = "a-value"`

    The returned value is `xsentinels.Default` if no override is found;
    this is so you can distinguish between overriding to None or no override set at all
    (`xsentinels.Default` evaluates to `False`, just like how `None` works).

    .. warning:: Only returns a value if overrides was directly set on self!

        (ie: **won't** consult the [Parent Chain](#parent-chain)).
        The parent chain (parent configs) are consulted when looking up a config value
        normally via `Config.get`. Overrides in self and then in parents are checked first.

        `get_override` method is here so you can examine a specific `Config` object and
        determine if there are any overrides set directly on it.

    Attributes:
        name (str): Name to use to get override [case-insensitive].

    Returns:
        Union[Any, xsentinels.Default]: The value, or `xsentinels.Default` if no value was
            set for `name`. This allows you to distinguish between overriding a value to
            `None` and no override being set in the first place
            (`xsentinels.Default` evaluates to `False`, just like how `None` works).
    """
    item = self._override.get_item(name)
    if item:
        return item.value
    return Default
def get_value(self, *args, **kwargs) ‑> Optional[str]

Deprecated since version: Deprecated in favor of using Config.get().

Right now we simply call Config.get() with same arguments for you and return result.

Expand source code
def get_value(self, *args, **kwargs) -> Optional[str]:
    """
    .. deprecated:: Deprecated in favor of using `Config.get()`.
        Right now we simply call `Config.get()` with same arguments for you and return result.
    """
    return self.get(*args, **kwargs)
def remove_default(self, name)

Remove default on self.

Warning: This WON'T affect any default set on a parent,

see Parent Chain.

You can also call this other ways, such as:

from xcon import config
from xsentinels import Default

# Alternate Method 1:
config.set_default("SOME_NAME", Default)
Expand source code
def remove_default(self, name):
    """
    Remove default on self.

    .. warning:: This WON'T affect any default set on a parent,
        see [Parent Chain](#parent-chain).

    You can also call this other ways, such as:

    ```python
    from xcon import config
    from xsentinels import Default

    # Alternate Method 1:
    config.set_default("SOME_NAME", Default)
    ```
    """
    xlog.info(f"Removing Config default for name ({name}).")
    self._defaults.remove_item_with_name(name=name)
def remove_override(self, name)

Remove override ONLY on self. This will not remove overrides from a parent.

Warning: This WON'T affect any override set on a parent!

see Parent Chain.

Someday we may make it easier to publicly go through the parents (right now there are internal/private methods that do this).

If you don't like an override, you can override the override by setting an override on a child/current Config object (see Config.set_override()).

Probably should not mess with config objects from higher up that you don't know anything about in any case. That's why I've hesitated about publicly exposing the parent chain too much.

You can remove overrides in various ways, such as:

from xcon import config
from xsentinels import Default

# Alternate Method 1:
config.SOME_NAME = Default

# Alternate Method 2:
config.set_override("SOME_NAME", Default)

At the moment these ways ^ will not remove an override from a parent.

If we do decide we want an ability to "white-out" an override; I would probably do it such that you could tell a child to not check parent(s) overrides on a specific value (ie: I would set the override value to Default on self internally, to indicate that).

Expand source code
def remove_override(self, name):
    """
    Remove override **ONLY** on self.
    This will not remove overrides from a parent.

    .. warning:: This WON'T affect any override set on a parent!
        see [Parent Chain](#parent-chain).

    Someday we may make it easier to publicly go through the parents
    (right now there are internal/private methods that do this).

    If you don't like an override, you can override the override by setting an override
    on a child/current `Config` object (see `Config.set_override`).

    Probably should not mess with config objects from higher up that you don't know
    anything about in any case.  That's why I've hesitated about publicly exposing
    the parent chain too much.


    You can remove overrides in various ways, such as:

    ```python
    from xcon import config
    from xsentinels import Default

    # Alternate Method 1:
    config.SOME_NAME = Default

    # Alternate Method 2:
    config.set_override("SOME_NAME", Default)
    ```

    At the moment these ways ^ will not remove an override from a parent.

    If we do decide we want an ability  to "white-out" an override;
    I would probably do it such that you could tell a child to not check parent(s)
    overrides on a specific value
    (ie: I would set the override value to `Default` on self internally, to indicate that).
    """
    xlog.info(f"Removing Config override for name ({name}).")
    self._override.remove_item_with_name(name=name)
def set_default(self, name: str, value: Optional[Any])

When someone tries to look up a config value [perhaps via Config.get] and if a value is not found anywhere… But someone called this to define a default for it we return the default value set here [or passed in via Config.init(defaults={…})].

For a few examples of how this can be used, see Config, Search Order section. Also see Config.init(…) doc for 'defaults' param.

Args

name : str
Case-insensitive name for the default config.
value : Optional[Any]
Default value, can be anything [but are generally strings]. If you provide None for this value param, that will be stored and will be returned if a default is needed for param [you can use this feature to override a parent-config default to None if needed].
Expand source code
def set_default(self, name: str, value: Optional[Any]):
    """
    When someone tries to look up a config value [perhaps via `Config.get`] and if a value
    is not found anywhere... But someone called this to define a default for it we return
    the default value set here [or passed in via Config.__init__(defaults={...})].

    For a few examples of how this can be used, see `Config`, Search Order section.
    Also see Config.__init__(...) doc for 'defaults' param.

    Args:
        name (str): Case-insensitive name for the default config.
        value (Optional[Any]): Default value, can be anything [but are generally strings].
            If you provide None for this value param, that will be stored and will be returned
            if a default is needed for param [you can use this feature to override a
            parent-config default to None if needed].

    """
    if value is Default:
        self.remove_default(name)
        return

    default_item = DirectoryItem(
        directory="/_default/user-set", name=name, value=value, source=f"config.set_default",
        cacheable=False
    )

    xlog.info(
        "Setting Config default for item ({default_item}).",
        extra=dict(default_item=default_item)
    )
    self._defaults.add_item(default_item)
def set_exports(self, *, services: Iterable[Union[str, DefaultType]])

This allows you to set all the exports. Right now we only support setting them by service [and not environment]. See Config.add_export() for more details.

This replaces all current services. By default, the export list is this:

( xsentinels.Default, )

Default when resolved means we ask the parent chain for any exports. If you set the exports without including this then the parent-chain won't be consulted.

See Config.add_service for a way to easily append to end of list and keeps what's currently in it.

Args

services (Iterable[Union[str, xsentinels.Default]]): List of exports you want to add by service name. If you don't add the xsentinels.Default somewhere in this list then we will NOT check the parent-chain

Expand source code
def set_exports(self, *, services: Iterable[Union[str, DefaultType]]):
    """
    This allows you to set all the exports. Right now
    we only support setting them by service [and not environment]. See `Config.add_export`
    for more details.

    This replaces all current services. By default, the export list is this:

    ( `xsentinels.Default`, )

    `Default` when resolved means we ask the parent chain for any exports.
    If you set the exports without including this then the parent-chain won't be consulted.

    See `Config.add_service` for a way to easily append to end of list and keeps what's
    currently in it.

    Args:
        services (Iterable[Union[str, `xsentinels.Default`]]): List of exports you want
            to add by service name. If you don't add the `xsentinels.Default` somewhere in
            this list then we will NOT check the parent-chain
    """
    self._exports = {x: None for x in xloop(services, default_not_iterate=[str])}
def set_override(self, name, value: [typing.Any, Default])

Sets an override on self. When someone asks for this value, this will be returned regardless of what any provider or environmental variable as set.

You can also set an override by setting a value for a config-name directly on Config via this syntax:

>>> from xcon.config import config
>>> config['some_override_name'] = "my override value"

For details see Naming Rules.

Important: This will also affect child config objects!

They will look for overrides set on a parent before looking at any providers.

For more details see Parent Chain and Overrides topics.

Args

name
Name of the item to remove, case-insensitive.
value : Union[Any, xsentinels.Default]
Can be any value. If Default is used we will instead call Config.remove_override()(name) for you to remove the value.
Expand source code
def set_override(self, name, value: [Any, Default]):
    """
    Sets an override on self. When someone asks for this value, this will be returned
    regardless of what any provider or environmental variable as set.

    You can also set an override by setting a value for a config-name directly on `Config`
    via this syntax:

    >>> from xcon.config import config
    >>> config['some_override_name'] = "my override value"

    For details see [Naming Rules](#naming-rules).

    .. important:: This will also affect child config objects!
        They will look for overrides set on a parent before looking at any providers.

        For more details see [Parent Chain](#parent-chain) and [Overrides](#overrides) topics.

    Args:
        name: Name of the item to remove, case-insensitive.
        value (Union[Any, xsentinels.Default]): Can be any value. If Default is used
            we will instead call `Config.remove_override(name)` for you to remove the value.
    """
    if value is Default:
        self.remove_override(name)
        return

    # Checked for default already, from this point forward it's just `Any` type.
    value: Any
    override_item = DirectoryItem(
        directory="/_override", name=name, value=value, source=f"Config.set_override",
        cacheable=False
    )

    xlog.info(f"Setting Config override for item ({override_item}).")
    self._override.add_item(
        override_item
    )
class ConfigRetriever

Retrieving the setting from config

Expand source code
class ConfigRetriever(SettingsRetrieverProtocol):
    """Retrieving the setting from config"""
    def __call__(self, *, field: SettingsField, settings: 'XconSettings') -> Any:
        return config.get(field.name)

Ancestors

Methods

def __call__(self, *, field: SettingsField, settings: BaseSettings) ‑> Any

Inherited from: SettingsRetrieverProtocol.__call__

This is how the BaseSettings field, when retrieving its value, will call us. You must override this (or simply use a normal function with the same …

class ConfigSettings (retrievers: Optional[Union[List[SettingsRetrieverProtocol], SettingsRetrieverProtocol]] = None, **kwargs)

Base Settings class. For all class properties defined there will be a corresponding _settings_field["name"] = SettingsField created value that will control how this value is read and manipulated.

The purpose of the Settings class is to allow a library or project/service to define a number of settings that are needed in order to function. You define a number of Settings propertiess to indicate what settings are available to use in the project.

You define a Settings and properties very similar to how you define a dataclass. You specify a property name, type_hint, and default_value.

>>> class MySettings(BaseSettings):
...    name: type_hint = default_value

A default SettingsField will be configured using the name, type_hint, and default_value as follows – SettingsField(name=name, type_hint=type_hint, converter=type_hint, default_value=default_value, resolver=default_resolver)

This functionality can be overridden by setting the default_value to a custom SettingsField. The custom SettingsField will be merged with the default SettingsField overriding any fields that were defined in the custom SettingsField.

It's important to note that while we are setting these attributes on the class they will not remain as attributes on the class. The _SettingsMeta will take each attribute and convert them to a SettingsField and then place them in the class's _setting_fields attribute.

Example of various ways to allocate a SettingsField on a Settings subclass:

>>> class MySettings(BaseSettings):
...     setting_1: int
Allocates
>>> SettingsField(name="setting_1", type_hint=int, resolver=SettingsResolver)
>>> class MySettings(BaseSettings):
...     setting_1: int = 3
Allocates
>>> SettingsField(name="setting_1", type_hint=int, resolver=SettingsResolver, default_value=3)
>>> class MySettings(BaseSettings):
...     setting_1 = 3
Allocates
>>> SettingsField(name="setting_1", type_hint=int, resolver=SettingsResolver, default_value=3)
>>> class MySettings(BaseSettings):
...     setting_1: int = SettingsField(name="other", required=False)
Allocates
>>> SettingsField(name="other", type_hint=int, resolver=SettingsResolver, required=False)

Accessing Class (not instance) Attributes = Lazy Property Reference

You can do lazy forward-refrences by simply asking the Settings class (not instance) for a attribute. Doing so will return a SettingsClassProperty that is a forward reference to the singleton instance class attribute.

Examples of how you might use this

>>> class MySettings(BaseSettings):
...    my_url_setting: str
>>> class MySubSettings(BaseSettings):
...    my_field: str
>>> class SomeClass:
...    some_attr = MySettings.my_url_setting
>>>
>>> MySettings.grab().my_url_setting = "my-url"
>>> MySubSettings.grab().my_field = MySettings.my_url_setting
>>>
>>> assert SomeClass.some_attr == "my-url"
>>> assert MySubSettings.grab().some_attr == "my-url"
>>> assert MySettings.grab().some_attr == "my-url"

Setting public Class Attributes after creation time not allowed

Attempting to set a public class level attribute will result in an Error being raised.

Settings Attribute (Property) Inheritance not allowed

To keep things as simple as possible we don't allow SettingsClass attribute inheritance. You can however create a parent class that defines methods / @properties that can be inherited. Trying to set a regular (non-method/non-property/public) attribute will raise an error.

Accessing Instance Properties = Get Value and Convert

When calling MySettings.grab().my_setting the Settings class will attempt to retrieve and convert the corresponding value. Getting the the source value and converting the value is controlled by the SettingsField. Here is how it works.

Start by attempting to retrieve a property from the class instance

>>> MyClass.grab().my_setting

The Settings class will do the following 1. Attempt to retrieve a value. a. Lookup value from self via object.__getattribute__(self, key) b. If an AttributeError is thrown then lookup the value from the corresponding field via self._settings_field[name].get_value() 2. Convert the retrieved value by calling self._settings_field[name].convert_value(value)

TODO

We don't support property setters/deleters at the moment. We would need to implement a __setattr__ here, where it would check for a property setter/getter on field object. (Consider a explicit fget and fset attribute on SettingsField at that point)

Set attributes to values that are passed via key-word arguments, these are the initial values for the settings instance your creating; they are set directly on the instance as if you did this:

# These two statements do the same thing:
obj = SomeSettings(some_keyword_arg="hello")

obj = SomeSettings()
obj.some_keyword_arg="hello"

Args

retrievers
can be used to populate new instance's retrievers,

see BaseSettings.settings__instance_retrievers.

Expand source code
class ConfigSettings(Settings, default_retrievers=[ConfigRetriever()]):
    pass

Ancestors

Static methods

def __init_subclass__(thread_sharable=Default, attributes_to_skip_while_copying: Optional[Iterable[str]] = Default, **kwargs)

Inherited from: BaseSettings.__init_subclass__

Args

thread_sharable
If False: While a dependency is lazily auto-created, we will ensure we do it per-thread, and not make it visible …
def grab() ‑> ~T

Inherited from: BaseSettings.grab

Gets a potentially shared dependency from the current udpend.context.XContext

def proxy() ‑> ~R

Inherited from: BaseSettings.proxy

Returns a proxy-object, that when and attribute is asked for, it will proxy it to the current object of cls

def proxy_attribute(attribute_name: str) ‑> Any

Inherited from: BaseSettings.proxy_attribute

Returns a proxy-object, that when and attribute is asked for, it will proxy it to the current attribute value on the current object of cls

Instance variables

var obj

Inherited from: BaseSettings.obj

class property/attribute that will return the current dependency for the subclass it's asked on by calling Dependency.grab, passing no extra …

var settings__instance_retrievers : List[SettingsRetrieverProtocol]

Inherited from: BaseSettings.settings__instance_retrievers

You can add one or more retrievers to this instance of settings (won't modify default_retrievers for the entire class, only modifies this specific …

Methods

def __call__(self, func)

Inherited from: BaseSettings.__call__

This makes Resource subclasses have an ability to be used as function decorators by default unless this method is overriden to provide some other …

def __copy__(self)

Inherited from: BaseSettings.__copy__

Basic shallow copy protection (I am wondering if I should just remove this default copy code) …