Settings
********

The "Settings" class provides a general-purpose data container for
various kinds of information that need to be stored and processed by
the PLAMS environment. Other PLAMS objects (like for example "Job",
"JobManager" or "GridRunner") have their own "Settings" instances that
store data defining and adjusting their behavior. The global scope
"Settings" instance ("config") is used for global settings.

Some settings which require well-defined structures have their own
derived "Settings" classes. These still possess all the flexibility of
the base class (in having dynamic custom attributes), but also contain
a set of required fields with default values, and tooltips. Any
derived settings class is interchangeable with the base "Settings"
class of the same structure.


Tree-like structure
===================

The "Settings" class is based on the regular Python dictionary (built-
in class "dict", tutorial can be found here) and in many aspects works
just like it:

   >>> s = Settings()
   >>> s['abc'] = 283
   >>> s[147147] = 'some string'
   >>> print(s['abc'])
   283
   >>> del s[147147]

The main difference is that data in "Settings" can be stored in
multilevel fashion, whereas an ordinary dictionary is just a flat
structure of key-value pairs. That means a sequence of keys can be
used to store a value. In the example below "s['a']" is itself a
"Settings" instance with two key-value pairs inside:

   >>> s = Settings()
   >>> s['a']['b'] = 'AB'
   >>> s['a']['c'] = 'AC'
   >>> s['x']['y'] = 10
   >>> s['x']['z'] = 13
   >>> s['x']['foo'][123] = 'even deeper'
   >>> s['x']['foo']['bar'] = 183
   >>> print(s)
   a:
     b:    AB
     c:    AC
   x:
     foo:
         123:  even deeper
         bar:  183
     y:    10
     z:    13
   >>> print(s['x'])
   foo:
       123:    even deeper
       bar:    183
   y:  10
   z:  13

So for each key the value can be either a "proper value" (string,
number, list etc.) or another "Settings" instance that creates a new
level in the data hierarchy. That way similar information can be
arranged in subgroups that can be copied, moved and updated together.
It is convenient to think of a "Settings" object as a tree. The root
of the tree is the top instance ("s" in the above example), "proper
values" are stored in leaves (a leaf is a childless node) and internal
nodes correspond to nested "Settings" instances (we will call them
*branches*). Tree representation of "s" from the example above is
illustrated on the following picture:

[image]

Tree-like structure could also be achieved with regular dictionaries,
but in a rather cumbersome way:

   >>> d = dict()
   >>> d['a'] = dict()
   >>> d['a']['b'] = dict()
   >>> d['a']['b']['c'] = dict()
   >>> d['a']['b']['c']['d'] = 'ABCD'
   ===========================
   >>> s = Settings()
   >>> s['a']['b']['c']['d'] = 'ABCD'

In the last line of the above example all intermediate "Settings"
instances are created and inserted automatically. Such behavior,
however, has some downsides -- every time you request a key that is
not present in a particular "Settings" instance (for example as a
result of a typo), a new empty instance is created and inserted as a
value of this key. This is different from dictionaries where an
exception is raised in such a case:

   >>> d = dict()
   >>> d['foo'] = 'bar'
   >>> x = d['fo']
   KeyError: 'fo'
   ===========================
   >>> s = Settings()
   >>> s['foo'] = 'bar'
   >>> x = s['fo']

   >>> print(s)
   fo:            # the value here is an empty Settings instance
   foo:    bar


Dot notation
============

To avoid inconvenient punctuation, keys stored in "Settings" can be
accessed using the dot notation in addition to the usual bracket
notation. In other words "s.abc" works as a shortcut for "s['abc']".
Both notations can be used interchangeably:

   >>> s.a.b = 'AB'
   >>> s['a'].c = 'AC'
   >>> s.x['y'] = 10
   >>> s['x']['z'] = 13
   >>> s['x'].foo[123] = 'even deeper'
   >>> s.x.foo.bar = 183
   >>> print(s)
   a:
     b:    AB
     c:    AC
   x:
     foo:
         123:  even deeper
         bar:  183
     y:    10
     z:    13

Due to the internal limitation of the Python syntax parser, keys other
than single word strings cannot work with that shortcut, for example:

   >>> s.123.b.c = 12
   SyntaxError: invalid syntax
   >>> s.q we.r.t.y = 'aaa'
   SyntaxError: invalid syntax
   >>> s.5fr = True
   SyntaxError: invalid syntax

In those cases one has to use the regular bracket notation:

   >>> s[123].b.c = 12
   >>> s['q we'].r.t.y = 'aaa'
   >>> s['5fr'] = True

The dot shortcut does not work for keys which begin and end with two
(or more) underscores (like "__key__"). This is done on purpose to
ensure that Python magic methods work properly.


Case sensitivity
================

"Settings" are case-preserving but case-insensitive. That means every
key is stored in its original form, but when looked up (for example,
to access value, test existence or delete), any casing can be used:

   >>> s = Settings()
   >>> s.foo = 'bar'
   >>> s.System.one = 1
   >>> s.system.two = 2
   >>> print(s.FOO)
   bar
   >>> 'Foo' in s
   True
   >>> print(s)
   foo:    bar
   System:
      one:     1
      two:     2
   >>> 'oNe' in s.SYSTEM
   True


API
===

class Settings(*args, **kwargs)

   Automatic multi-level dictionary. Subclass of built-in class
   "dict".

   The shortcut dot notation ("s.basis" instead of "s['basis']") can
   be used for keys that:

   * are strings

   * don't contain whitespaces

   * begin with a letter or an underscore

   * don't both begin and end with two or more underscores.

   Iteration follows lexicographical order (via "sorted()" function)

   Methods for displaying content ("__str__()" and "__repr__()") are
   overridden to recursively show nested instances in easy-readable
   format.

   Regular dictionaries (also multi-level ones) used as values (or
   passed to the constructor) are automatically transformed to
   "Settings" instances:

      >>> s = Settings({'a': {1: 'a1', 2: 'a2'}, 'b': {1: 'b1', 2: 'b2'}})
      >>> s.a[3] = {'x': {12: 'q', 34: 'w'}, 'y': 7}
      >>> print(s)
      a:
        1:    a1
        2:    a2
        3:
          x:
            12:   q
            34:   w
          y:  7
      b:
        1:    b1
        2:    b2

   Parameters:
      * **args** (*Any*) --

      * **kwargs** (*Any*) --

   __init__(*args, **kwargs)

      Parameters:
         * **args** (*Any*) --

         * **kwargs** (*Any*) --

   copy()

      Return a new instance that is a copy of this one. Nested
      "Settings" instances are copied recursively, not linked.

      In practice this method works as a shallow copy: all "proper
      values" (leaf nodes) in the returned copy point to the same
      objects as the original instance (unless they are immutable,
      like "int" or "tuple"). However, nested "Settings" instances
      (internal nodes) are copied in a deep-copy fashion. In other
      words, copying a "Settings" instance creates a brand new "tree
      skeleton" and populates its leaf nodes with values taken
      directly from the original instance.

      This behavior is illustrated by the following example:

         >>> s = Settings()
         >>> s.a = 'string'
         >>> s.b = ['l','i','s','t']
         >>> s.x.y = 12
         >>> s.x.z = {'s','e','t'}
         >>> c = s.copy()
         >>> s.a += 'word'
         >>> s.b += [3]
         >>> s.x.u = 'new'
         >>> s.x.y += 10
         >>> s.x.z.add(1)
         >>> print(c)
         a:  string
         b:  ['l', 'i', 's', 't', 3]
         x:
           y:    12
           z:    set([1, 's', 'e', 't'])
         >>> print(s)
         a:  stringword
         b:  ['l', 'i', 's', 't', 3]
         x:
           u:    new
           y:    22
           z:    set([1, 's', 'e', 't'])

      This method is also used when "copy.copy()" is called.

      Parameters:
         **self** (*TSelf*) --

      Return type:
         *TSelf*

   soft_update(other)

      Update this instance with data from *other*, but do not
      overwrite existing keys. Nested "Settings" instances are soft-
      updated recursively.

      In the following example "s" and "o" are previously prepared
      "Settings" instances:

         >>> print(s)
         a:  AA
         b:  BB
         x:
           y1:   XY1
           y2:   XY2
         >>> print(o)
         a:  O_AA
         c:  O_CC
         x:
           y1:   O_XY1
           y3:   O_XY3
         >>> s.soft_update(o)
         >>> print(s)
         a:  AA        #original value s.a not overwritten by o.a
         b:  BB
         c:  O_CC
         x:
           y1:   XY1   #original value s.x.y1 not overwritten by o.x.y1
           y2:   XY2
           y3:   O_XY3

      *Other* can also be a regular dictionary. Of course in that case
      only top level keys are updated.

      Shortcut "A += B" can be used instead of "A.soft_update(B)".

      Parameters:
         * **self** (*TSelf*) --

         * **other** (*Settings*) --

      Return type:
         *TSelf*

   update(other)

      Update this instance with data from *other*, overwriting
      existing keys. Nested "Settings" instances are updated
      recursively.

      In the following example "s" and "o" are previously prepared
      "Settings" instances:

         >>> print(s)
         a:  AA
         b:  BB
         x:
           y1:   XY1
           y2:   XY2
         >>> print(o)
         a:  O_AA
         c:  O_CC
         x:
           y1:   O_XY1
           y3:   O_XY3
         >>> s.update(o)
         >>> print(s)
         a:  O_AA        #original value s.a overwritten by o.a
         b:  BB
         c:  O_CC
         x:
           y1:   O_XY1   #original value s.x.y1 overwritten by o.x.y1
           y2:   XY2
           y3:   O_XY3

      *Other* can also be a regular dictionary. Of course in that case
      only top level keys are updated.

      Parameters:
         **other** (*Mapping**[**Hashable**, **Any**]*) --

   merge(other)

      Return new instance of "Settings" that is a copy of this
      instance soft-updated with *other*.

      Shortcut "A + B" can be used instead of "A.merge(B)".

      Parameters:
         * **self** (*TSelf*) --

         * **other** (*Settings*) --

      Return type:
         *TSelf*

   remove(other)

      Update this instance removing keys from *other*. Nested
      "Settings" instances are updated recursively.

      Shortcut "A -= B" can be used instead of "A.remove(B)".

      Parameters:
         * **self** (*TSelf*) --

         * **other** (*Settings*) --

      Return type:
         *TSelf*

   difference(other)

      Return new instance of "Settings" that is a copy of this
      instance with keys of *other* removed.

      Shortcut "A - B" can be used instead of "A.difference(B)".

      Parameters:
         * **self** (*TSelf*) --

         * **other** (*Settings*) --

      Return type:
         *TSelf*

   find_case(key)

      Check if this instance contains a key consisting of the same
      letters as *key*, but possibly with different case. If found,
      return such a key. If not, return *key*.

      Parameters:
         **key** (*Hashable*) --

      Return type:
         *Hashable*

   get(key, default=None)

      Like regular "get", but ignore the case.

      Parameters:
         * **key** (*Hashable*) --

         * **default** (*Any** | **None*) --

      Return type:
         *Any* | None

   pop(key, *args)

      Like regular "pop", but ignore the case.

      Parameters:
         * **key** (*Hashable*) --

         * **args** (*Any*) --

      Return type:
         *Any* | None

   popitem()

      Like regular "popitem", but ignore the case.

      Return type:
         *Any*

   setdefault(key, default=None)

      Like regular "setdefault", but ignore the case and if the value
      is a dict, convert it to "Settings".

      Parameters:
         * **key** (*Hashable*) --

         * **default** (*Any** | **None*) --

      Return type:
         None

   as_dict()

      Return a copy of this instance with all "Settings" replaced by
      regular Python dictionaries.

      Return type:
         *Dict*

   classmethod suppress_missing()

      A context manager for temporary disabling the
      "Settings.__missing__()" magic method: all calls now raising a
      "KeyError".

      As a results, attempting to access keys absent from an arbitrary
      "Settings" instance will raise a "KeyError", thus reverting to
      the default dictionary behaviour.

      Note:

        The "Settings.__missing__()" method is (temporary) suppressed
        at the class level to ensure consistent invocation by the
        Python interpreter. See also special method lookup.

      Example:

         >>> s = Settings()

         >>> with s.suppress_missing():
         ...     s.a.b.c = True
         KeyError: 'a'

         >>> s.a.b.c = True
         >>> print(s.a.b.c)
         True

      Return type:
         *SuppressMissing*

   contains_nested(key_tuple, suppress_missing=False)

      Check if a nested key is present by recursively iterating
      through this instance using the keys in *key_tuple*.

      The get item method is called recursively on this instance until
      all keys in key_tuple are exhausted.

      Setting *suppress_missing* to "True" will raise a "KeyError" if
      a key in *key_tuple* cannot be accessed in this instance,

         >>> s = Settings()
         >>> s.a.b.c = 1
         >>> value = s.contains_nested(('a', 'b', 'c'))
         >>> print(value)
         True

      Parameters:
         * **key_tuple** (*Tuple**[**Hashable**, **...**]*) --

         * **suppress_missing** (*bool*) --

      Return type:
         bool

   get_nested(key_tuple, suppress_missing=False, default=None)

      Retrieve a nested value by, recursively, iterating through this
      instance using the keys in *key_tuple*.

      The get item method is called recursively on this instance until
      all keys in key_tuple are exhausted.

      Setting *suppress_missing* to "True" will raise a "KeyError" if
      a key in *key_tuple* cannot be accessed in this instance,
      Otherwise, the default value will be returned.

         >>> s = Settings()
         >>> s.a.b.c = True
         >>> value = s.get_nested(('a', 'b', 'c'))
         >>> print(value)
         True

      Parameters:
         * **key_tuple** (*Tuple**[**Hashable**, **...**]*) --

         * **suppress_missing** (*bool*) --

         * **default** (*Any** | **None*) --

      Return type:
         *Any* | None

   set_nested(key_tuple, value, suppress_missing=False)

      Set a nested value by, recursively, iterating through this
      instance using the keys in *key_tuple*.

      The get item method followed finally by set item is called
      recursively on this instance until all keys in key_tuple are
      exhausted.

      Setting *suppress_missing* to "True" will raise a "KeyError" if
      a key in *key_tuple* cannot be accessed in this instance,
      Otherwise, no set operation will be performed.

         >>> s = Settings()
         >>> s.set_nested(('a', 'b', 'c'), True)
         >>> print(s)
         a:
           b:
             c:      True

      Parameters:
         * **key_tuple** (*Tuple**[**Hashable**, **...**]*) --

         * **value** (*Any** | **None*) --

         * **suppress_missing** (*bool*) --

      Return type:
         None

   pop_nested(key_tuple, suppress_missing=False, default=None)

      Pop a nested value by, recursively, iterating through this
      instance using the keys in *key_tuple*.

      The get item method followed finally by pop item is called
      recursively on this instance until all keys in key_tuple are
      exhausted.

      Setting *suppress_missing* to "True" will raise a "KeyError" if
      a key in *key_tuple* cannot be accessed in this instance,
      Otherwise, the default value will be returned.

         >>> s = Settings()
         >>> s.a.b.c = True
         >>> value = s.pop_nested(('a', 'b', 'c'))
         >>> print(value)
         True
         >>> print(s)
         <empty Settings>

      Parameters:
         * **key_tuple** (*Tuple**[**Hashable**, **...**]*) --

         * **suppress_missing** (*bool*) --

         * **default** (*Any** | **None*) --

      Return type:
         *Any* | None

   nested_keys(flatten_list=True, include_empty=False)

      Get the nested keys corresponding to all nodes in this instance,
      both 'branches' and 'leaves'.

      If *flatten_list* is set to "True", all nested lists will be
      flattened and elements converted to nodes.

      If *include_empty* is set to "True", nodes without values are
      also returned.

         >>> s = Settings()
         >>> s.a.b.c = True
         >>> value = list(s.nested_keys())
         >>> print(value)
         [('a',), ('a', 'b'), ('a', 'b', 'c')]

      Parameters:
         * **flatten_list** (*bool*) --

         * **include_empty** (*bool*) --

      Return type:
         *Generator*[*Tuple*[*Hashable*, ...], None, None]

   block_keys(flatten_list=True, include_empty=False)

      Get the nested keys corresponding to the internal nodes in this
      instance, also referred to as 'blocks' or 'branches'. These
      internal nodes correspond to "Settings" objects.

      If *flatten_list* is set to "True", all nested lists will be
      flattened and elements converted to internal nodes.

      If *include_empty* is set to "True", nodes without values in the
      "Settings" are also returned.

         >>> s = Settings()
         >>> s.a.b.c = True
         >>> value = list(s.branch_keys())
         >>> print(value)
         [('a',), ('a', 'b')]

      Parameters:
         * **flatten_list** (*bool*) --

         * **include_empty** (*bool*) --

      Return type:
         *Generator*[*Tuple*[*Hashable*, ...], None, None]

   compare(other)

      Compare this settings object to another to get the difference
      between them.

      The result is a dictionary containing three entries:
         * added: the flattened keys present in this settings object
           and not in the other, with their values

         * removed: the flattened keys present in the other settings
           object and not in this, with their values

         * modified: the flattened keys present in both settings
           objects, with both values in this and the other object

         >>> s = Settings()
         >>> t = Settings()
         >>> s.a.b = 1
         >>> s.c.d = 2
         >>> t.c.d = 3
         >>> t.e.f = 4
         >>> value = s.compare(t)
         >>> print(value)
         {'added': {('a', 'b'): 1}, 'modified': {('c', 'd'): (2, 3)}, 'removed': {('e', 'f'): 4}}

      Parameters:
         **other** (*Settings*) --

      Return type:
         *Dict*[str, *Dict*[*Any*, *Any*] | *Dict*[*Any*,
         *Tuple*[*Any*, *Any*]]]

   flatten(flatten_list=True)

      Return a flattened copy of this instance.

      New keys are constructed by concatenating the (nested) keys of
      this instance into tuples.

      Opposite of the "Settings.unflatten()" method.

      If *flatten_list* is "True", all nested lists will be flattened
      as well. Dictionary keys are replaced with list indices in such
      case.

         >>> s = Settings()
         >>> s.a.b.c = True
         >>> print(s)
         a:
           b:
             c:      True

         >>> s_flat = s.flatten()
         >>> print(s_flat)
         ('a', 'b', 'c'):    True

      Parameters:
         **flatten_list** (*bool*) --

      Return type:
         *Settings*

   unflatten(unflatten_list=True)

      Return a nested copy of this instance.

      New keys are constructed by expanding the keys of this instance
      (*e.g.* tuples) into new nested "Settings" instances.

      If *unflatten_list* is "True", integers will be interpretted as
      list indices and are used for creating nested lists.

      Opposite of the "Settings.flatten()" method.

         >>> s = Settings()
         >>> s[('a', 'b', 'c')] = True
         >>> print(s)
         ('a', 'b', 'c'):    True

         >>> s_nested = s.unflatten()
         >>> print(s_nested)
         a:
           b:
             c:      True

      Parameters:
         **unflatten_list** (*bool*) --

      Return type:
         *Settings*

   __iter__()

      Iteration through keys follows lexicographical order. All keys
      are sorted as if they were strings.

      Return type:
         *Iterator*[*Hashable*]

   __missing__(name)

      When requested key is not present, add it with an empty
      "Settings" instance as a value.

      This method is essential for automatic insertions in deeper
      levels. Without it things like:

         >>> s = Settings()
         >>> s.a.b.c = 12

      will not work.

      The behaviour of this method can be suppressed by initializing
      the "Settings.suppress_missing" context manager.

      Parameters:
         **name** (*Hashable*) --

      Return type:
         *Settings*

   __contains__(name)

      Like regular "__contains`__", but ignore the case.

      Parameters:
         **name** (*Hashable*) --

      Return type:
         bool

   __getitem__(name)

      Like regular "__getitem__", but ignore the case.

      Parameters:
         **name** (*Hashable*) --

      Return type:
         *Any*

   __setitem__(name, value)

      Like regular "__setitem__", but ignore the case and if the value
      is a dict, convert it to "Settings".

      Parameters:
         * **name** (*Hashable*) --

         * **value** (*Any*) --

      Return type:
         None

   __delitem__(name)

      Like regular "__detitem__", but ignore the case.

      Parameters:
         **name** (*Hashable*) --

      Return type:
         *Any*

   __getattr__(name)

      If name is not a magic method, redirect it to
      "__getattribute__".

      Parameters:
         **name** (*str*) --

      Return type:
         *Any*

   __setattr__(name, value)

      If name is not a magic method, redirect it to "__setattr__".

      Parameters:
         * **name** (*str*) --

         * **value** (*Any*) --

      Return type:
         None

   __delattr__(name)

      If name is not a magic method, redirect it to "__delattr__".

      Parameters:
         **name** (*str*) --

      Return type:
         None

   _str(indent)

      Print contents with *indent* spaces of indentation. Recursively
      used for printing nested "Settings" instances with proper
      indentation.

      Parameters:
         **indent** (*int*) --

      Return type:
         str

   __str__()

      Return str(self).

      Return type:
         str

   __dir__()

      Return standard attributes, plus dynamically added keys which
      can be accessed via dot notation.

      Return type:
         *List*[str]

   __repr__()

      Return repr(self).

      Return type:
         str

   __isub__(other)

      Update this instance removing keys from *other*. Nested
      "Settings" instances are updated recursively.

      Shortcut "A -= B" can be used instead of "A.remove(B)".

      Parameters:
         * **self** (*TSelf*) --

         * **other** (*Settings*) --

      Return type:
         *TSelf*

   __sub__(other)

      Return new instance of "Settings" that is a copy of this
      instance with keys of *other* removed.

      Shortcut "A - B" can be used instead of "A.difference(B)".

      Parameters:
         * **self** (*TSelf*) --

         * **other** (*Settings*) --

      Return type:
         *TSelf*

Note:

  Methods "update()" and "soft_update()" are complementary. Given two
  "Settings" instances "A" and "B", the command "A.update(B)" would
  result in "A" being exactly the same as "B" would be after
  "B.soft_update(A)".


Global settings
===============

Global settings are stored in a public "ConfigSettings" instance named
"config". They contain variables adjusting general behavior of PLAMS
as well as default settings for various objects (jobs, job manager
etc.)

The default values are explained in the description of each property
on "config". It is recommended to have a look at these options, to
give an overview of what behavior can be configured.

To change a setting for a script, just set the relevant option on the
config to the preferred value, after the import statements. For
example:

   config.log.stdout = 1
   config.job.pickle = False
   config.default_jobrunner = JobRunner(parallel=True, maxjobs=8)

A context manager "config_context" is provided to override the global
"config" within a specific scope:

config_context()

   Enter a context with a "config" specific only to the current
   context.

   The global "config" will be copied into a new "ConfigSettings",
   which can then be modified independently of the global instance.

   The "config" for this context can be retrieved using the function
   "get_config()".

   Note:

     Starting a new thread creates a new context, so the context
     configuration will not automatically be used in the new thread.
     To copy over the parent thread context to the new thread, instead
     use "ContextAwareThread"

      >>> with config_context() as cfg:
      >>>     cfg.log.stdout = 0
      >>>     print(f"Stdout logging inside context disabled: {get_config().log.stdout == 0}")
      >>> print(f"Stdout logging outside context disabled: {get_config().log.stdout == 0}")
          Stdout logging inside context disabled: True
          Stdout logging outside context disabled: False

   Returns:
      copy of the global "config" instance in a new "ConfigSettings"

   Return type:
      *Generator*[*ConfigSettings*, None, None]

This can be useful for temporarily disabling or changing global
settings. If you use this context manager, obtain "config" with the
"get_config" function:

get_config()

   Get the "ConfigSettings" for the current code context.

   This will be the configuration used within a "config_context()"
   context, or otherwise the global "config".

   Returns:
      "ConfigSettings" that should be used in the current code context

   Return type:
      *ConfigSettings*

The structure for all the defined options on the nested settings
objects is defined below.

class ConfigSettings(*args, **kwargs)

   Extends the default "Settings" with standard options which are
   required for global config. The values for these options are
   initialized to default values. The default "JobRunner" and
   "JobManager" are lazily initialized when first accessed.

   Parameters:
      * **args** (*Any*) --

      * **kwargs** (*Any*) --

   __init__(*args, **kwargs)

      Parameters:
         * **args** (*Any*) --

         * **kwargs** (*Any*) --

   property init: bool

      Whether config has been marked as fully initialized and jobs are
      ready to be run. Defaults to "False".

   property _explicit_init: bool

      Whether config has been explicitly initialized by the user by
      calling "init()". Defaults to "False".

   property preview: bool

      When enabled, no actual calculations are run, only inputs and
      runscripts are prepared. Defaults to "False".

   property sleepstep: int

      Unit of time which is used whenever some action needs to be
      repeated until a certain condition is met. Defaults to "5".

   property ignore_failure: bool

      When enabled, accessing a failed/crashed job gives a log message
      instead of an error. Defaults to "True".

   property daemon_threads: bool

      When enabled, all threads started by JobRunner are daemon
      threads, which are terminated when the main thread finishes, and
      hence allow immediate end of the parallel script when Ctrl-C is
      pressed. Defaults to "True".

   property atexit_timeout: int

      Maximum seconds to wait in atexit function calls, to allow
      threads to finish up before shutting them down. Defaults to "30"
      seconds.

   property erase_workdir: bool

      When enabled, the entire main working folder is deleted at the
      end of script. Defaults to "False".

   property jobmanager: JobManagerSettings

      See "JobManagerSettings".

   property job: JobSettings

      See "JobSettings".

   property log: LogSettings

      See "LogSettings".

   property saferun: SafeRunSettings

      See "SafeRunSettings".

   property default_jobrunner: JobRunner

      Default "JobRunner" that will be used for running jobs, if an
      explicit runner is not provided.

   property default_jobmanager: JobManager

      Default "JobManager" that will be used when running jobs, if an
      explicit manager is not provided.

   _check_initialized(key)

      Check if the value of a given key is initialized i.e. the key is
      present, the value is not "None" any lazy value is calculated.

      Parameters:
         **key** (*str*) -- key to check

      Returns:
         whether value is initialized

      Return type:
         bool

class JobSettings(*args, **kwargs)

   Job settings for global config.

   Parameters:
      * **args** (*Any*) --

      * **kwargs** (*Any*) --

   __init__(*args, **kwargs)

      Parameters:
         * **args** (*Any*) --

         * **kwargs** (*Any*) --

   property pickle: bool

      Enables pickle for the whole job object to [jobname].dill, after
      job execution is finished. Defaults to "True".

   property pickle_protocol: int

      Protocol used for pickling. Defaults to "-1".

   property keep: str

      Defines which files should be kept on disk. Defaults to "all".

   property save: str

      Defines which files should be kept on disk. Defaults to "all".

   property runscript: RunScriptSettings

      See "RunscriptSettings".

   property link_files: bool

      When enabled, re-run files will be hardlinked instead of copied,
      unless on Windows when files are always copied. Defaults to
      "True".

   class OnStatusChangeCallback(*args, **kwargs)

      Definition for a callback which is fired on a "Job" status
      change.

      __init__(*args, **kwargs)

      __subclasshook__()

         Abstract classes can override this to customize issubclass().

         This is invoked early on by abc.ABCMeta.__subclasscheck__().
         It should return True, False or NotImplemented.  If it
         returns NotImplemented, the normal algorithm is used.
         Otherwise, it overrides the normal algorithm (and the outcome
         is cached).

      __weakref__

         list of weak references to the object (if defined)

      __call__(*, name=Ellipsis, path=Ellipsis, status=Ellipsis, at=Ellipsis, **kwargs)

         Call self as a function.

         Parameters:
            * **name** (*str*) --

            * **path** (*str** | **None*) --

            * **status** (*str*) --

            * **at** (*datetime*) --

            * **kwargs** (*Any*) --

         Return type:
            *Any*

   property on_status_change: OnStatusChangeCallback | None

      Callback function which is called whenever the status of a job
      changes. This can be used for example to send a notification
      whenever a job is complete or errors.

      The callback should adhere to the "OnStatusChangeCallback"
      protocol. Example usage:

         >>> def notify(name: str, path: Optional[str], status: str, at: datetime, **_) -> None:
         >>>     ...
         >>> config.job.on_status_change = notify

class JobManagerSettings(*args, **kwargs)

   Job manager settings for global config.

   Parameters:
      * **args** (*Any*) --

      * **kwargs** (*Any*) --

   __init__(*args, **kwargs)

      Parameters:
         * **args** (*Any*) --

         * **kwargs** (*Any*) --

   property counter_len: int

      Number of digits for the counter used when two or more jobs have
      the same name, when all the jobs apart from the first one are
      renamed to [jobname].002 ([jobname].003 etc.) Defaults to "3".

   property hashing: str

      Hashing method used for testing if some job was previously run.
      Defaults to "input".

   property remove_empty_directories: bool

      When enabled, removes of all empty subdirectories in the main
      working folder at the end of the script. Defaults to "True".

class LogSettings(*args, **kwargs)

   Log settings for global config.

   Parameters:
      * **args** (*Any*) --

      * **kwargs** (*Any*) --

   __init__(*args, **kwargs)

      Parameters:
         * **args** (*Any*) --

         * **kwargs** (*Any*) --

   property file: int

      Verbosity of the log printed to .log file in the main working
      folder. Defaults to "5".

   property stdout: int

      Verbosity of the log printed to the standard output. Defaults to
      "3".

   property csv: int

      Verbosity of the log printed to .csv job log file in the main
      working folder. Defaults to "7".

   property time: bool

      When enabled, include write time for each log event. Defaults to
      "True".

   property date: bool

      When enabled, include write date for each log event. Defaults to
      "True".

class RunScriptSettings(*args, **kwargs)

   Run script settings for global config.

   Parameters:
      * **args** (*Any*) --

      * **kwargs** (*Any*) --

   __init__(*args, **kwargs)

      Parameters:
         * **args** (*Any*) --

         * **kwargs** (*Any*) --

   property shebang: str

      First line of all produced runscripts. Defaults to "#!/bin/sh".

   property stdout_redirect: bool

      When enabled, the standard output redirection is handled by the
      operating system (by using '>[jobname].out' in the runscript),
      instead of being handled by native Python mechanism. Defaults to
      "False".

class SafeRunSettings(*args, **kwargs)

   Safe run settings for global config.

   Parameters:
      * **args** (*Any*) --

      * **kwargs** (*Any*) --

   __init__(*args, **kwargs)

      Parameters:
         * **args** (*Any*) --

         * **kwargs** (*Any*) --

   property repeat: int

      Number of attempts for each run() call. Defaults to "10".

   property delay: int

      Delay between attempts for each run() call. Defaults to "1".
