Source code for bsb.options

"""
This module contains the global options.

You can set options at the ``script`` level (which superceeds all other levels such as
environment variables or project settings).

.. code-block::

  import bsb.options
  from bsb import BsbOption

  class MyOption(BsbOption, cli=("my_setting",), env=("MY_SETTING",), script=("my_setting", "my_alias")):
      def get_default(self):
          return 4

  # Register the option into the `bsb.options` module
  MyOption.register()

  assert bsb.options.my_setting == 4
  bsb.options.my_alias = 6
  assert bsb.options.my_setting == 6

Your ``MyOption`` will also be available on all CLI commands as ``--my_setting`` and will
be read from the ``MY_SETTING`` environment variable.
"""

import functools

from ._options import ProfilingOption, VerbosityOption

# Store the module magic for unpolluted namespace copy
_module_magic = globals().copy()

import sys
import types

from . import option as _bsboptmod
from .exceptions import OptionError, ReadOnlyOptionError
from .plugins import discover
from .reporting import report

_options = {}
_project_options = {}
_module_options = {}
_module_option_values = {}

# Everything defined between pre-freeze and post-freeze may be considered to be added to
# the `_OptionsModule` instance, place all public API functions between them.
_pre_freeze = set(globals().keys())


def _get_module_option(tag):  # pragma: nocover
    global _module_options

    if tag not in _module_options:
        if not discover_options.cache_info().misses:
            discover_options()
            return _get_module_tag(tag)
        else:
            raise OptionError(f"Unknown module option '{tag}'")
    return _module_options[tag]


def _get_module_tag(tag):  # pragma: nocover
    return _get_module_option(tag).__class__.script.tags[0]


[docs] def get_option_classes(): """ Return all of the classes that are used to create singleton options from. Useful to access the option descriptors rather than the option values. :returns: The classes of all the installed options by name. :rtype: dict[str, bsb.option.BsbOption] """ return discover("options")
[docs] def get_option_descriptor(name): """ Return an option :param name: Name of the option to look for. :type name: str :returns: The option singleton of that name. :rtype: dict[str, bsb.option.BsbOption] """ global _options discover_options() if name in _options: return _options[name] else: raise OptionError(f"Unknown option '{name}'")
[docs] def register_option(name, option): """ Register an option as a global BSB option. Options that are installed by the plugin system are automatically registered on import of the BSB. :param name: Name for the option, used to store and retrieve its singleton. :type name: str :param option: Option instance, to be used as a singleton. :type option: :class:`.option.BsbOption` """ global _options if name in _options: if type(_options[name]) != type(option): raise OptionError( f"The '{name}' option name is already taken by {_options[name].__class__}." ) else: _options[name] = option for tag in type(option).script.tags: _register_module_option(tag, option) if type(option).project.tags: _register_project_option(option)
[docs] def unregister_option(option): """ Unregister a globally registered option. Also removes its script and project parts. :param option: Option singleton, to be removed. :type option: :class:`.option.BsbOption` """ global _options, _project_options del _options[option.name] _remove_module_tags(*type(option).script.tags) path = type(option).project.tags if path: section = _project_options for slug in path[:-1]: if slug in section: section = section[slug] else: return try: del section[path[-1]] except KeyError: pass
def _register_project_option(option): # pragma: nocover """ Register an option that can be manipulated from ``pyproject.toml``, unregistered options can be used, but :func:`.options.store` and :func:`.options.read` won't work. :param option: Option. :type option: :class:`.option.BsbOption` """ global _project_options path = type(option).project.tags section = _project_options for slug in path[:-1]: section = section.setdefault(slug, {}) if path[-1] in section: raise OptionError( f"The '{'.'.join(path)}' tag is already taken by {section[path[-1]].__class__}." ) else: section[path[-1]] = option
[docs] def get_project_option(tag): """ Find a project option :param tag: dot-separated path of the option. e.g. ``networks.config_link``. :type tag: str :returns: Project option instance :rtype: :class:`.option.BsbOption` """ global _project_options discover_options() path = tag.split(".") section = _project_options for slug in path: if slug in section: section = section[slug] else: raise OptionError(f"The project option `{tag}` does not exist.") return section
def _register_module_option(tag, option): # pragma: nocover """ Register an option that can be manipulated from :mod:`bsb.options`. """ global _module_options if tag in _module_options: raise OptionError( f"The '{tag}' tag is already taken by {_module_options[tag].__class__}." ) else: _module_options[tag] = option def _remove_module_tags(*tags): # pragma: nocover """ Removes tags. """ global _module_options, _module_option_values for tag in tags: try: del _module_options[tag] except KeyError: pass try: del _module_option_values[tag] except KeyError: pass
[docs] def reset_module_option(tag): global _module_option_values opt = _get_module_option(tag) # Module option values always stored under the "module tag" (= tag 0) try: del _module_option_values[type(opt).script.tags[0]] except KeyError: pass
[docs] def set_module_option(tag, value): """ Set the value of a module option. Does the same thing as ``setattr(options, tag, value)``. :param tag: Name the option is registered with in the module. :type tag: str :param value: New module value for the option :type value: Any """ global _module_option_values option = _get_module_option(tag) if option.readonly: raise ReadOnlyOptionError("'%tag%' is a read-only option.", option, tag) mod_tag = _get_module_tag(tag) _module_option_values[mod_tag] = getattr(option, "setter", lambda x: x)(value)
[docs] def get_module_option(tag): """ Get the value of a module option. Does the same thing as ``getattr(options, tag)`` :param tag: Name the option is registered with in the module. :type tag: str """ global _module_option_values, _module_options discover_options() tag = _get_module_tag(tag) if tag in _module_option_values: return _module_option_values[tag] else: return _module_options[tag].get()
[docs] def is_module_option_set(tag): """ Check if a module option was set. :param tag: Name the option is registered with in the module. :type tag: str :returns: Whether the option was ever set from the module :rtype: bool """ global _module_option_values return _get_module_tag(tag) in _module_option_values
[docs] def get_option_descriptors(): """ Get all the registered option singletons. """ global _options discover_options() return _options.copy()
[docs] @functools.cache def discover_options(): # Register the discoverable options plugins = discover("options") for plugin in plugins.values(): option = plugin() register_option(option.name, option)
[docs] def store_option(tag, value): """ Store an option value permanently in the project settings. :param tag: Dot-separated path of the project option :type tag: str :param value: New value for the project option :type value: Any """ get_project_option(tag).project = value
[docs] def read_option(tag=None): """ Read an option value from the project settings. Returns all project settings if tag is omitted. :param tag: Dot-separated path of the project option :type tag: str :returns: Value for the project option :rtype: Any """ if tag is None: path, content = _bsboptmod._pyproject_bsb() report(f"Reading project settings from '{path}'", level=4) return content else: return get_project_option(tag).get(prio="project")
[docs] def get_option(tag, prio=None): """ Retrieve the cascaded value for an option. :param tag: Name the option is registered with. :type tag: str :param prio: Give priority to a type of value. Can be any of 'script', 'cli', 'project', 'env'. :type prio: str :returns: (Possibly prioritized) value of the option. :rtype: Any """ option = get_option_descriptor(tag) return option.get(prio=prio)
_post_freeze = set(globals().keys()).difference(_pre_freeze) class _OptionsModule(types.ModuleType): __name__ = "bsb.options" def __getattr__(self, attr): if attr in ["__path__", "__warningregistry__", "__qualname__"]: # __path__: # Python uses `hasattr(module, '__path__')` to see if a module is a package # so we need to raise an AttributeError to make `hasattr` return False. # __warningregistry__: # The `unittest` module checks existence. # __qualname__: # Sphinx checks this raise super().__getattribute__(attr) return self.get_module_option(attr) def __setattr__(self, attr, value): self.set_module_option(attr, value) def __delattr__(self, attr): try: opt = _get_module_option(attr) except OptionError: raise super().__delattr__(attr) from None else: del opt.script _om = _OptionsModule(__name__) # Copy the module magic from the original module. _om.__dict__.update(_module_magic) # Copy over the intended API from the original module. for _key, _value in zip(_post_freeze, map(globals().get, _post_freeze)): _om.__dict__[_key] = _value # Set the module's public API. _om.__dict__["__all__"] = sorted([k for k in vars(_om).keys() if not k.startswith("_")]) sys.modules[__name__] = _om register_option("verbosity", VerbosityOption()) register_option("profiling", ProfilingOption()) # Static public API __all__ = [ "get_option", "get_module_option", "get_option_descriptor", "get_option_classes", "get_option_descriptors", "get_project_option", "is_module_option_set", "read_option", "register_option", "reset_module_option", "set_module_option", "store_option", "unregister_option", ]