Registry¶
At the heart of good code organization is a flexible yet powerful way to add new features or functionality to a past, current, and even future projects. omni-fig
accomplishes this by relying on registries to manage all of the most important pieces of code that may be explicitly addressed/referred to in the config.
The first registry registers all scripts which are essentially the top level interface for how a user is to interact with the code (by calling scripts). These scripts can be called from the terminal (see Running Scripts for more details and examples) or executed in environments like jupyter notebooks or an IDE debugger (eg. Pycharm).
Next, is the component registry. A component is any atomic piece of code that might be specified in the config. As the config object is a yaml file, there are no python classes or objects there in (aside from dicts, lists and primitives). Instead, if the user wants to use a user defined class or function, it can be registered as a component, and then the component can be referred to in the config with the key _type
.
Finally, modifiers can be registered and used to wrap existing components to further customize the behavior of components dynamically from the config. There are two special kinds of modifiers: AutoModifier()
and Modification()
. An AutoModifier()
essentially acts as a child class of whatever component it is wrapping. Meanwhile a Modification()
is used to wrap or modify a component after it has been created. The example below (and elsewhere) will hopefully help elucidate how modifiers can be used with components.
For an extended example in how all three registries might be used for a simple project where we sample from a guassian distribution and then record the particularly low probability events, which might be implemented and registered like so:
import sys
import random
import omnifig as fig
@fig.Script('sample-low-prob')
def sample_low_prob(A): # config object
mylogger = A.pull('logger') # create a logger object according to specifications in the config
num_events = A.pull('num_events', 10) # default value is 10 if "num_events" is not specified in the config
interest_criterion = A.pull('criterion', 5.)
important_criterion = A.pull('important_criterion', 5.2)
mu, sigma = A.pull('mu',0.), A.pull('sigma', 1.)
sigma = max(sigma, 1e-8) # ensure sigma is positive
print('Sampling...')
events = []
count = 0
while len(events) < num_events:
x = random.gauss(mu, sigma)
if abs(x) > interest_criterion:
mylogger.log_line(f'Found important {x:.2f}\n', important=abs(x)>important_criterion)
events.append(x)
count += 1
mylogger.log_line(f'Finding {num_events} low prob samples required {count} samples.\n',
include_credits=True, important=True)
mylogger.close()
return events
In this example project, we may require a logger (called logger
above) to print information to stdout
or a file, and we can register components to implement the different choices and corresponding arguments.
@fig.AutoComponent('stdout') # automatically pulls all arguments in signature before creating
def _get_stdout(): # in this case, we don't need any arguments
return sys.stdout
@fig.AutoComponent('file')
def _get_file(path):
return open(path, 'a+')
@fig.Component('mylogger')
class Logger:
def __init__(self, A): # "A" refers to the config object
self.always_log = A.pull('always_log', False) # value defaults to False if not found in the config
self.print_stream = A.pull('print_stream', None) # values can also be components themselves
self.credits = A.pull('credits', []) # pulled values can also be dicts or lists (with defaults)
if not isinstance(self.credits, list):
self.credits = list(self.credits)
def log_line(self, line, stream=None, important=False, include_credits=False):
if stream is None:
stream = self.print_stream
if stream is not None and (important or self.always_log):
stream.write(line)
if include_credits and len(self.credits):
stream.write('Credits: {}\n'.format(', '.join(self.credits)))
def close(self):
if self.print_stream is not None:
self.print_stream.close()
This example shows how Component()
and AutoComponent()
may be used with both classes and functions. The config (eg. registered as myconfig1
) may contain something like:
num_events: 5
logger:
_type: mylogger
credits: [Gauss, Hamilton, Fourier]
print_stream._type: stdout # "." is treated like a sub-dict
Or (say, myconfig2
):
always_log: True # as this argument is in a parent dict of "logger" it will still be found within "logger".
logger:
_type: mylogger
print_stream:
_type: file
path: 'log_file.txt' # "." is treated like a sub-dict
Additionally, components can be modified in the config using Modifier()
, AutoModifier()
, and Modification()
. Modifiers essentially act as additional decorators that can dynamically be specified in the config to change the bahavior of components before (eg. Modifier()
or AutoModifier()
) or after (Modification()
) creating the component.
To add on to our previous example:
@fig.AutoModifier('multi')
class MultiStream:
def __init__(self, A):
streams = A.pull('print_streams', '<>print_stream', []) # use prefix "<>" to default to a different key
if not isinstance(streams, (list, tuple)):
streams = [streams]
A.push('print_stream', None) # push to replace values in the config
super().__init__(A) # initialize any dynamically added superclasses (-> Logger)
self.print_streams = streams
def log_line(self, line, stream=None, important=False, include_credits=False):
if stream is not None:
return super().log_line(line, stream=stream, important=important, include_credits=include_credits)
for stream in self.print_streams:
return super().log_line(line, stream=stream, important=important, include_credits=include_credits)
def close(self):
for stream in self.print_streams:
stream.close()
@fig.Modification('remove-credits')
def remove_names(logger, A):
for name in A.pull('remove_names', []):
if name in logger.credits:
logger.credits.remove(name)
print(f'Removed {name} from credits')
return logger
And some associated configs might include (config3
):
parents: [config1] # all these registered configs will be loaded and merged with this one
path: 'backup-log.txt'
logger:
_mod: multi
print_streams:
- _type: file
- _type: stdout
Or, finally (config4
):
parents: [config2, config3]
remove_names: [Fourier]
logger._mod: [multi, remove-credits]
Now, if your head isn’t spinning from the complicated merging and defaulting of configs, then perhaps you can figure out what path we will actually end up using as our log file when using config4
?
The answer is backup-log.txt
because the AutoModifier()
multi
starts from the logger.print_streams
branch, which does not get merged with the logger.print_stream
branch (which contains path : 'log_file.txt'
), so when defaulting towards the root, log_file.txt
is not encountered. For more information, the code for this example can be found in examples/gauss_fun
.
Another part of this example that warrants careful consideration is how the AutoModifier()
multi
is used. The trick is that an AutoModifier()
actually dynamically creates a new child class of the registered AutoModifier()
type and the original type of the component (for that reason AutoModifier()
must be a class, not a function, and they only work on components that are classes). In this case, the dynamically created type will be called MultiStream_Logger
with the method resolution order (MRO) [MultiStream, Logger, object]
.
Note that the AutoModifier()
can be paired with, in principle, any component (although some will raise errors), which effectively means an AutoModifier()
allows changing the behavior of any component, even ones that haven’t even been written yet. While AutoModifier()
is one of the most powerful features of the registry system in omni-fig
, they are consequently also rather advanced, so particular care must be taken when using them.
-
Script
(name, description=None, use_config=True)[source]¶ Decorator to register a script
- Parameters
name – name of script
description – a short description of what the script does
use_config –
True
if the config should be passed as only arg when calling the script function, otherise it will automatically pull all arguments in the script function signature
- Returns
decorator function expecting a callable
-
AutoScript
(name, description=None)[source]¶ Convienence decorator to register scripts that automatically extract relevant arguments from the config object
- Parameters
name – name of the script
description – a short description of what the script does
- Returns
decorator function expecting a callable that does not expect the config as argument (otherwise use
Script()
)
-
Component
(name=None)[source]¶ Decorator to register a component
NOTE: components should usually be types/classes to allow modifications
- Parameters
name – if not provided, will use the __name__ attribute.
- Returns
decorator function
-
AutoComponent
(name=None, aliases=None, auto_name=True)[source]¶ Instead of directly passing the config to an AutoComponent, the necessary args are auto filled and passed in. This means AutoComponents are somewhat limited in that they cannot modify the config object and they cannot be modified with AutoModifiers.
Note: AutoComponents are usually components that are created with functions (rather than classes) since they can’t be automodified. When registering classes as components, you should probably use Component instead, and pull from the config directly.
- Parameters
name – name to use when registering the auto component
aliases – optional aliases for arguments used when autofilling (should be a dict[name,list[aliases]])
- Returns
decorator function
-
Modifier
(name=None, expects_config=False)[source]¶ Decorator to register a modifier
NOTE: a
Modifier
is usually not a type/class, but rather a function (exceptAutoModifiers
, see below)- Parameters
name – if not provided, will use the __name__ attribute.
expects_config – True iff this modifier expects to be given the config as second arg
- Returns
decorator function
-
AutoModifier
(name=None)[source]¶ Can be used to automatically register modifiers that combine types
To keep component creation as clean as possible, modifier types should allow arguments to their __init__ (other than the Config object) and only call pull on arguments not provided, that way child classes of the modifier types can specify defaults for the modifications without calling pull() multiple times on the same arg.
Note: in a way, this converts components to modifiers (but think before using). This turns the modified component into a child class of this modifier and its previous type.
In short, Modifiers are used for wrapping of components, AutoModifiers are used for subclassing components
- Parameters
name – if not provided, will use the __name__ attribute.
- Returns
decorator to decorate a class
-
Modification
(name=None)[source]¶ A kind of Modifier that modifies the component after it is created, and then returns the modified component
expects a callable with input (component, config)
Modifications should almost always be applied after all other modifiers, so they should appear at the end of _mod list
- Parameters
name – name to register
- Returns
a decorator expecting the modification function