Package: optionsfactory
OptionsFactoryhttps://github.com/johnomotani/optionsfactoryOptionsFactoryallows you to define a set of options, which can have (if you like):
default values (which may be expressions depending on other options); documentation for
each option; an allowed type or list of types; a check that the value option is on an
allowed list; checks that the value of an option satisfies some tests.InstallationOptionsFactorycan be installed usingpippip install --user optionsfactoryorcondaconda install -c conda-forge optionsfactoryUsageOnce the options are defined in anOptionsFactory, you create a particular instance of
the options by passing some user settings (a dict or YAML file). TheOptionsFactoryuses the values passed, sets the remaining options from the default values or
expressions and returns anOptionsobject. Options are immutable so that you do not
have to worry about the options being accidentally changed during execution - however,
seeMutableOptionsFactoryif you want to be able to update the
options dynamically.For example, some simple options might be implemented like this:fromoptionsfactoryimportOptionsFactoryclassA:# The keyword arguments define the options and give the default valuesoptions_factory=OptionsFactory(a=1,b=2)def__init__(self,user_options=None):self.options=self.options_factory.create(user_options)# options can be accessed like a dictmyvalue=2*options["a"]#... or as attributesmynewvalue=3+options.aIt might also be useful for some classes to allow the options to be set from keyword
arguments, for examplefromoptionsfactoryimportOptionsFactoryclassB:# The keyword arguments define the options and give the default valuesoptions_factory=OptionsFactory(a=1,b=2)def__init__(self,**kwargs):self.options=self.options_factory.create(kwargs)Thecreate()method will not alter the argument passed to it.The options will then combine explicitly set values and defaults:>>>b1=B()# uses default values>>>b1.options.a1>>>b1.options.b2>>>b2=B(b=4)# override one of the defaults>>>b2.options.a1>>>b2.options.b4More flexibility is available by using expressions to set the default values.fromoptionsfactoryimportOptionsFactoryclassC:# The keyword arguments define the options and give the default valuesoptions_factory=OptionsFactory(a=lambdaoptions:options.b+5,b=2)def__init__(self,**kwargs):self.options=self.options_factory.create(kwargs)could be used like:>>>c1=C()# only default values>>>c1.options.a7>>>c1.options.b2>>>c2=C(a=1,b=3)# override both options, expression not used>>>c2.options.a1>>>c2.options.b>>>3>>>c3=C(b=4)# User-set value of b evaluated in default expression for a>>>c3.options.a9>>>c3.options.b4Circular dependencies in expressions will be detected and raise a ValueError.WithMetaWithMetaobjects are used to store the defaults withinOptionsFactory, and can be
used to define options with extra information, e.g.fromoptionsfactoryimportOptionsFactory,WithMetafromoptionsfactory.checksimportis_positive,is_NoneclassD:options_factory=OptionsFactory(a=WithMeta(1,doc="option a"),b=WithMeta(2,value_type=int),c=WithMeta(3,allowed=[1,2,3]),d=WithMeta(4,check_all=is_positive),e=WithMeta(5,check_any=lambdax:x<6),f=WithMeta(6,doc="option f",value_type=[int,float],allowed=[6,7,8,9.5]),g=WithMeta(7,doc="option g",value_type=[int,None],check_all=[is_positive,lambdax:x<10],check_any=[lambdax:x<2,lambdax:x>6],),)def__init__(self,**kwargs):self.options=self.options_factory.create(kwargs)The first argument toWithMetagives the default value for the option, and the
remaining keyword arguments are all optional. UsingWithMetathe values behave just as
the simple default values described above:>>>d=D(b=12)>>>d.options.a1>>>d.options["b"]12documentationDocumentation defined in the factory initialisation can be accessed from either theOptionsFactoryor theOptionsinstance via adocproperty, that gives adictwith the documentation for each option:>>>D.options_factory.doc["a"]# Get doc from the factory'option a'>>>D.options_factory.doc["b"]# No doc was defined for this option>>>d=D()>>>d.options.doc["f"]# Get doc from the Options instance'option f'value_typeThevalue_typeargument can be used to give a type or sequence of types that
the option is allowed to have. Trying to set an option with a non-allowed type
raises aValueError:>>>d2=D(d=-2)ValueError:Thevalue-2ofkey=disnotcompatiblewithcheck_all>>>d3=D(f=8)>>>d3.options.f8>>>d4=D(f=9.5)>>>d4.options.f9.5>>>d5=D(f="a string")TypeError:astringisnotoftype(<class'int'>, <class 'float'>) for key=fchecking valuesThere are two ways of checking the values that are set for options.allowedTheallowedkeyword sets a list of allowed options. AValueErroris raised if the
value being set is not in the list.allowedcannot be set if either ofcheck_allorcheck_anyis. Example:>>>d6=D(c=2)ValueError:2isnotintheallowedvalues(1,2,3)forkey=c>>>d7=D(c=4)ValueError:4isnotintheallowedvalues(1,2,3)forkey=ccheck_allandcheck_anyThese arguments can be passed a list of expressions. The expressions passed tocheck_allmust all evaluate toTrue, or anValueErroris raised. At least one of
the expressions passed tocheck_anyis raised. The choice ofcheck_anyorcheck_allis a matter of convenience and clarity - the effect of either could be
achieved with a single, sufficiently complicated, expression. They can both be set at
the same time, although this probably not often useful. Neither can be set if theallowedkeyword is. Example:>>>d8=D(d=14)>>>d8.options.d14>>>d9=D(d=-1)ValueError:Thevalue-1ofkey=disnotcompatiblewithcheck_all>>>d10=D(e=5)>>>d10.options.e5>>>d11=D(e=6)ValueError:Thevalue6ofkey=eisnotcompatiblewithcheck_any>>>d12=D(g=1)>>>d12.options.g1>>>d13=D(g=-1)ValueError:Thevalue-1ofkey=gisnotcompatiblewithcheck_all>>>d14=D(g=5)ValueError:Thevalue5ofkey=gisnotcompatiblewithcheck_any>>>d15=D(g=9)>>>d15.options.g9>>>d16=D(g=10)ValueError:Thevalue10ofkey=gisnotcompatiblewithcheck_allDefault expressionsMuch more flexibility is offered for default values by using expressions. These are
single-argument functions (lambda expressions are often useful), which return the
desired default value when passed anOptionsobject, from which the values of other
options (which may or may not be expressions themselves) can be accessed. See theclass Cexample above. Whennested optionsare used, expressions can also
access values in subsections or parent sections of the options tree:fromoptionsfactoryimportOptionsFactoryclassE:options_factory=OptionsFactory(a=1,b=lambdaoptions:options.a+options.subsection1.c+options.subsection2.e,subsection1=OptionsFactory(c=lambdaoptions:options.parent.a+options.parent.subsection2.e,subsubsection=OptionsFactory(d=2),),subsection2=OptionsFactory(e=lambdaoptions:options.parent.subsection1.subsubsection.d,),)def__init__(self,**kwargs):self.options=self.options_factory.create(kwargs)If we initialiseEwith just the defaults>>>e=E()>>>e.options.a1>>>e.options.b6>>>e.options.subsection1.c3>>>e.options.subsection1.subsubsection.d2>>>e.options.subsection2.e2OptionsFactory extension for subclassesSometimes it can be useful to create an extended version of anOptionsFactory. For
example a child class might have some extra options that are not needed in its parent
class, or might require different default values. TheOptionsFatory.add()method
creates a newOptionsFactoryfrom an existing one, with the keyword arguments adding
to or overriding the options in the original factory. When overriding existing options,
pass a value or expression to keep existing documentation and checks, or aWithMetaobject to provide new documentation and checks. Example:fromoptionsfactoryimportOptionsFactoryclassParent:options_factory=OptionsFactory(a=WithMeta(1,doc="option a"),b=WithMeta(2,doc="option b"),c=WithMeta(3,doc="option c"),)def__init__(self,**kwargs):self.optiotns=self.options_factory(kwargs)classChild(Parent):options_factory=Parent.options_factory.add(# Keep 'a' unchangedb=4,# Change the default value of 'b', but keep the documentationc=WithMeta(5,doc="child option c"),# New default and attributes for 'c'd=WithMeta(6,doc="new option d"),# New option not present in the parent)and if we create aChildinstance>>>child=Child()>>>child.options.a1>>>child.options.doc["a"]'option a'>>>child.options.b4>>>child.options.doc["b"]'option b'>>>child.options.c5>>>child.options.doc["c"]'child option c'>>>child.options.d6>>>child.options.doc["d"]'new option d'Nested optionsNested options are created by passing anotherOptionsFactoryas a keyword argument in
the OptionsFactory constructor. See thenested structureexample
below.Collecting defaultsIt can be useful to collect options from several factories together into a higher-level
factory. For example if aclass Containercontains members of several classes, it
might be useful for theoptions_factoryofContainerto have options for all those
members, but to define the options, defaults, documentation, etc. in the particular
classes. This can be done by passingOptionsFactoryobjects as positional arguments to
theOptionsFactoryconstructor - see theflat structureexample
below.Other Featuresload from YAMLThe user settings can be loaded from a YAML file (ifPyYAMLis available - install theoptionsfactory[yaml]variant to ensure this):>>>withopen(filename)asf:>>>options=options_factory.create_from_yaml(f)save to YAMLThe options can also be saved to a YAML file, either the non-default values only>>>withopen(filename,'w')asf:>>>options.to_yaml(f)or all values including defaults, by passingTrueto thewith_defaultsargument>>>withopen(filename,'w')asf:>>>options.to_yaml(f,True)# saves options with default values as wellPickling (with dill)Optionsobjects can be pickled usingdill. This is tested. Pickling ofMutableOptionsobjects is not currently supported.ExamplesHere are a couple of more complicated examples of the patterns thatOptionsFactorywas
designed for.flat structureclass Acontains members of typesBandC, soA.options_factorycollects the
default values, documentation, etc. fromB.options_factoryandC.options_factoryby
taking them as positional arguments to the constructor. Then theOptionsobject ofAis used to create theOptionsobjects forBandC, which will have only the
options relevant to themselves in, becauseB.options_factoryandC.options_factoryignore any undefined options in the argument passed tocreate.classA:options_factory=OptionsFactory(B.options_factory,C.options_factory,a_opt1=WithMeta(1,allowed=[1,3,7]),a_opt2=WithMeta(2,value_type=[int,float]),)def__init__(self,user_options):self.options=self.options_factory(user_options)self.b=B(self.options)self.c=C(self.options)classB:options_factory=OptionsFactory(b_opt=WithMeta(3.0,checks=is_positive),)def__init__(self,options):self.options=self.options_factory.create(options)classC:options_factory=OptionsFactory(c_opt=WithMeta("c-value",value_type=str))def__init__(self,options):self.options=self.options_factory.create(options)IfBorCwere intended to also be used as user-facing classes, which want to get
their options from**kwargs, it would also be possible to havedef__init__(self,**kwargs):self.options=self.options_factory(kwargs)and create the objects inA's constructor likeself.b = B(**self.options).nested structureSimilar to the flat structure above, but keeping the options for different member
objects separated in different sections:classA:options_factory=OptionsFactory(B=B.options_factory,C=C.options_factory,a_opt1=WithMeta(1,allowed=[1,3,7]),a_opt2=WithMeta(2,value_type=[int,float]),)def__init__(self,user_options):self.options=self.options_factory(user_options)self.b=B(self.options.B)self.c=C(self.options.C)classB:options_factory=OptionsFactory(b_opt=WithMeta(3.0,checks=is_positive),)def__init__(self,options):self.options=optionsclassC:options_factory=OptionsFactory(c_opt=WithMeta("c-value",value_type=str))def__init__(self,options):self.options=optionsIfAneeds to change the default options for one of the nested sections, can use theadd()method likeB={"b_opt": 7.0).Default expressions in a nested options structure can use values from sub-sections, or
from parent sections, seeDefault expressions.IfBorCshould be allowed to be created independently of a containing class likeA, then you can instead define the constructor asclassB:options_factory=OptionsFactory(b_opt=WithMeta(3.0,checks=is_positive),)def__init__(self,options):self.options=self.options_factory(options)This will have the same effect as the above code when called withself.b = B(self.options.B)but also allows creating aBlikeanother_b = B({"b_opt": 4.0}).
(This version will be slightly more expensive than the one above because theOptionsobject will be converted to adict-like iterable and a newOptionscreated by
parsing that iterable.)global optionsNot recommended, but you could create a global options object for your package/program.
For example in a filemypackage.pyfromoptionsfactoryimportOptionsFactoryglobal_options=Noneoptions_factory=OptionsFactory(opt1=1,opt2=2,)defsetup(input_options):globalglobal_optionsglobal_options=options_factory.create(input_options)MutableOptionsFactoryMutableOptionsFactoryis almost identical toOptionsFactory, but createsMutableOptionsobjects which can be modified after being created (it also has acreate_immutable()method, equivalent toOptionsFactory.create()to createOptionsobjects).MutableOptionsbehave likeOptionswith the exception that
values can be set, or reset to the default value (usingdel) after the object is
created. Default values are re-calculated if any option is changed. Example:>>>fromoptions_factoryimportMutableOptionsFactory>>>factory=MutableOptionsFactory(a=1,b=lambdaoptions:2.0*options.a)>>>mutable_options=factory.create({"a":3,"b":4})>>>mutable_options.a3>>>mutable_options.b4>>>delmutable_options.b>>>mutable_options.b6.0>>>mutable_options.a=5>>>mutable_options.b10.0>>>mutable_options["a"]=7.5>>>mutable_options.b15.0>>>delmutable_options["a"]>>>mutable_options.b2.0>>>mutable_options.a1Expressions for non-default valuesPassing expressions for non-default values should work, although it has not been tested
yet. Expressions cannot at present be loaded from YAML files.AcknowledgementsThanks toBen DudsonandPeter
Hillfor discussion on options handling in thehypnotoad
projectand ideas infrozen_options.