Package: json-settings
json-settingsjson-settings is a Python framework for JSON configuration file handling. It
provides the following featuresDefine a nested Python class structure that mirrors the desired configuration
file.Automatic type checking.Implicit recursive error messaging that provides human readable information on
the location and nature of an error in a configuration file.Easy and adaptable value bounding validation.Array and range support for numerical values.Ability to convert a setting object with range values into a multidimensional
array of the settings object with singular values for each setting.Contentsjson-settingsContentsInstallationGetting StartedPrimitive-TypesReference-TypesNull-TypesTerminus-SettingSetting-Error-MessagesNumber-SettingsSpacesRange-MatchingString-Set-SettingsList-SettingsDictionary-SettingsInstallationInstall the json-settings package with the commandpip install json_settings.Getting StartedPrimitive-TypesWe would like to create a simple configuration file, my_json_config_file.json,
with a single setting that is an integer. The JSON file will look like this:{"my_integer":1}So we use the basic unit of json_settings, theSettingsbase class, to define
a newSettingsderived classimportjsonfromjson_settingsimportSettingsclassMyCoolSetting(Settings):@Settings.assigndef__init__(self,value:dict):self.my_integer=intif__name__=="__main__":withopen("my_json_config_file.json",'r')asf:values=json.loads(f.read())my_cool_setting=MyCoolSetting(values)print(my_cool_setting.my_integer)print(type(my_cool_setting.my_integer))If we run the Python above the output will be1
<class `int`>A few things to note:All user defined settings classes must call their base class'assigndecorator on the__init__method.All user define settings class'__init__method take a single argument (in
addition toself).All settings defined in the__init__method must be equal to their required type.Any variables defined in the__init__method will be enforced at runtime.JSON entries cannot contain hyphens in their id string.Reference-TypesWe now want to have a settings file that is more complex. The configuration file
will look like this:{"footware":{"type":"formal","quantity":2},"gloves":true}The corresponding Python class structure is as follows:importjsonfromjson_settingsimportSettingsclassFootwareSettings(Settings):@Settings.assigndef__init__(self,values):self.type=strself.quantity=intclassMyCoolSetting(Settings):@Settings.assigndef__init__(self,value:dict):self.footware=FootwareSettingsself.gloves=boolif__name__=="__main__":withopen("my_json_config_file.json",'r')asf:values=json.loads(f.read())my_cool_setting=MyCoolSetting(values)print(my_cool_setting.footware.type)print(my_cool_setting.footware.quantity)print(my_cool_setting.gloves)If we run the Python above the output will beformal
2
TrueThis nesting can be of an arbitrary depth, and all error handling is automatic
and recursive, allowing for easy construction of complex configuration files.Null-TypesThe standard json package convertsnullvalues toNonetype values. By
defaultSettingsderived classes will assignNoneregardless of the required
type. This can be restricted by using aTerminusSettingderived type.Terminus-SettingSometimes we need to define a setting which has more rigorous constraints. To do
this we define aTerminusSettingderived class.We want to define a setting that is the name of a king, however we require that
it starts with "king_".{"my_king":"king_james"}The corresponding Python class structure is as follows:importjsonfromjson_settingsimportTerminusSettingfromjson_settingsimportSettingsclassMyCoolSetting(Settings):@Settings.assigndef__init__(self):self.my_king=KinglyNameclassKinglyName(TerminusSetting):@TerminusSetting.assigndef__init__(self,value:dict):self.type=strdefcheck(self):ifnotself.value.startswith("king_"):raiseValueError('Name must start with "king_"')if__name__=="__main__":withopen("my_json_config_file.json",'r')asf:values=json.loads(f.read())my_kingly_setting=MyCoolSetting(values)print(my_kingly_setting.my_king)If we run the Python above the output will beking_jamesA few things to note:TerminusSetting derived classes must define only one attributetypein the__init__method, which is the type of the variable stored.The abstractcheckmethod must be defined for all TerminusSetting derived
classes.The check method will catchValueErrorandTypeErrorand raiseSettingCheckError, which in turn is caught by the enclosingSettingsderived instance and raised as aSettingErrorMessage.The value of the setting in aTerminusSettingderived class is stored in thevalueattribute.When an attribute of aSettingsobject which is aTerminusSettingderived
type is accessed, the value stored in theTerminusSettingderived instances
is returned, NOT theTerminusSettingsderived instance itself.Setting-Error-MessagesOne of the key features of json_settings is the recursive exception handling. To
demonstrate we define the following configuration file and corresponding Python
class structure{"first":{"second":{"fourth":1,"fith":"fith"},"third":false}}importsysimportjsonfromjson_settingsimportSettingsfromjson_settingsimportTerminusSettingfromjson_settingsimportSettingErrorMessageclassMainSettings(Settings):@Settings.assigndef__init__(self,values):self.first=SecondSettingsclassSecondSettings(Settings):@Settings.assigndef__init__(self,values):self.second=FinalSettingsself.third=boolclassFinalSettings(Settings):@Settings.assigndef__init__(self,values):self.fourth=intself.fith=FithSettingclassFithSetting(TerminusSetting):@TerminusSetting.assigndef__init__(self,value):self.type=strdefcheck(self):if"f"notinself.value:raiseValueError('Must contain the letter "f"')if__name__=="__main__":withopen("my_json_config_file.json",'r')asf:values=json.loads(f.read())try:my_cool_settings=MainSettings(values)exceptSettingErrorMessagease:sys.exit(e)Instead of running the above code with the correct JSON configuration file, we
will use the following, which contains an error{"first":{"second":{"fourth":1,"fith":"badger"},"third":false}}Running the above code will yield the following outputfirst -> second -> fith -> Must contain the letter "f"If we have an error in a different setting like so{"first":{"second":{"fourth":1,"fith":"fith"},"third":1}}we get the following error messagefirst -> third -> Expecting : <class 'bool'> | Received: <class 'int'>In each case the location and nature of the error in the configuration file is
indicated in the error message yielded to the user.Number-Settingsjson_settings provides a special base class for numerical settings.NumberSettingsis itself derived fromTerminusSetting, but with some extra
functionality.Imagine we wish to create a settings object with afloatsetting, that must be
greater than or equal to zero:{"important_number":1.0}importsysimportjsonfromjson_settingsimportSettingsfromjson_settingsimportNumberSettingfromjson_settingsimportSettingErrorMessageclassMainSettings(Settings):@Settings.assigndef__init__(self,values):self.important_number=ImportantNumberSettingclassImportantNumberSetting(NumberSetting):@NumberSetting.assigndef__init__(self,value):self.type=floatdefcheck(self):self.lower_bound(0.0)if__name__=="__main__":withopen("my_json_config_file.json",'r')asf:values=json.loads(f.read())try:my_cool_settings=MainSettings(values)exceptSettingErrorMessagease:sys.exit(e)print(f"Important Number is:{my_cool_settings.important_number})Notes:NumberSettingcomes with several inbuild check methods for enforcing
numberical boundslower_boundupper_boundlower_bound_exclusiveupper_bound_exclusiveRunning the above code will returnImportant Number is: 1.0HoweverNumberSettingderived settings objects also accept array and range
definitions. For example, if we use the following configuration file{"important_number":{"array":[1.0,2.0,3.0]}}we get the following outputImportant Number is: [1.0, 2.0, 3.0]We also note that if one of entries in the array does not satisfy the range
condition we get the following outputimportant_number -> must be >= 0.We can also define a set of values in the following way{"important_number":{"min":0.0,"max":5.0,"num":6}}which gives the following outputImportant Number is: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0]where we can see a linear space has been created over the defined range.Errors in the range definition are caught and yielded to the user such asimportant_number -> No 'min' parameter provided for rangeNote:The order of the range or array does not matter.SpacesConsider a situation where we have someNumberSettingsettings in our
configuration file{"primary_number":{"array":[1.0,2.0,3.0]},"secondary_number":{"min":4.0,"max":6.0,"num":3}}importsysimportjsonfromjson_settingsimportSettingsfromjson_settingsimportNumberSettingfromjson_settingsimportSettingErrorMessageclassMainSettings(Settings):@Settings.assigndef__init__(self,values):self.primary_number=ImportantNumberSettingself.secondary_number=ImportantNumberSettingclassImportantNumberSetting(NumberSetting):@NumberSetting.assigndef__init__(self,value):self.type=floatdefcheck(self):self.lower_bound(0.0)if__name__=="__main__":withopen("my_json_config_file.json",'r')asf:values=json.loads(f.read())try:my_cool_settings=MainSettings(values)exceptSettingErrorMessagease:sys.exit(e)print(f"Primary Number:{my_cool_settings.primary_number}")print(f"Secondary Number:{my_cool_settings.secondary_number}")Running the above will output the following, indicating that the two arrays are
storedPrimary Number: [1.0, 2.0, 3.0]
Secondary Number: [4.0, 5.0, 6.0]However what if we want to generate a set ofMainSettingsinstances, each one
representing a single point in combined cartesian product space ofprimary_numberandsecondary_number. We can do so using theSpaceclass.importsysimportjsonfromjson_settingsimportSettingsfromjson_settingsimportNumberSettingfromjson_settingsimportSettingErrorMessagefromjson_settingsimportSpaceclassMainSettings(Settings):@Settings.assigndef__init__(self,values):self.primary_number=ImportantNumberSettingself.secondary_number=ImportantNumberSettingclassImportantNumberSetting(NumberSetting):@NumberSetting.assigndef__init__(self,value):self.type=floatdefcheck(self):self.lower_bound(0.0)if__name__=="__main__":withopen("my_json_config_file.json",'r')asf:values=json.loads(f.read())try:my_cool_settings=MainSettings(values)exceptSettingErrorMessagease:sys.exit(e)settings_space=Space(my_cool_settings)print(f"Primary Number[0, 0]:{settings_space[0,0].primary_number}")print(f"Secondary Number[0, 0]:{settings_space[0,0].secondary_number}")print(f"Type of [0, 0] element:{settings_space[0,0]}")print(f"Primary Number[0, 1]:{settings_space[0,1].primary_number}")print(f"Secondary Number[0, 1]:{settings_space[0,1].secondary_number}")print(f"Primary Number[2, 2]:{settings_space[2,2].primary_number}")print(f"Secondary Number[2, 2]:{settings_space[2,2].secondary_number}")print(f"Space dimensions:{settings_space.shape}")print(f"Space summary:{settings_space.cout_summary()}")print(f"Total number of elements:{len(settings_space)}")The above will outputPrimary Number[0, 0]: 1.0
Secondary Number[0, 0]: 4.0
Type of [0, 0] element: <__main__.MainSettings object at 0x000001BF79B40F10>
Primary Number[0, 1]: 1.0
Secondary Number[0, 1]: 5.0
Primary Number[2, 2]: 3.0
Secondary Number[2, 2]: 6.0
Space dimensions: (3, 3)
Space summary: Computational space dimensions: 3 x 3

axis: 0:
        primary_number
        values:  [1.0, 2.0, 3.0]
axis: 1:
        secondary_number
        values:  [4.0, 5.0, 6.0]

Total number of elements: 9TheSpaceinstances behaves as a multidimensionalnumpyarray.A few things to note:You can define an arbitrary number of ranged parameters. The resultantSpaceinstance will have the corresponding number of dimensions.You can restrict the space range expansion to a particular subset of the
available settings by passing the optionalrestrictparameter to theSpaceconstructorrestrict :dict[str,str]
A dictionary ofstr:strpairs that are used to exclude
subsetting branches from the exploration function for finding ranges.
If when searching the settings object for ranges, a setting with the
same name as a key inrestrictis found, only subsettings with
name equal to the corresponding value will be searched.Range-MatchingSometimes we might have several ranges, however we want to couple some of them
together such that the resultingSpaceinstance is of reduced dimension.{"primary_number":{"array":[1.0,2.0,3.0]},"secondary_number":{"min":4.0,"max":6.0,"num":3},"tertiary_number":{"array":[-1.0,-2.0,-3.0]}}importsysimportjsonfromjson_settingsimportSettingsfromjson_settingsimportNumberSettingfromjson_settingsimportSettingErrorMessagefromjson_settingsimportSpaceclassMainSettings(Settings):@Settings.assigndef__init__(self,values):self.primary_number=ImportantNumberSettingself.secondary_number=ImportantNumberSettingself.tertiary_number=MinorNumberSettingclassImportantNumberSetting(NumberSetting):@NumberSetting.assigndef__init__(self,value):self.type=floatdefcheck(self):self.lower_bound(0.0)classMinorNumberSetting(NumberSetting):@NumberSetting.assigndef__init__(self,value):self.type=floatdefcheck(self):passif__name__=="__main__":withopen("my_json_config_file.json",'r')asf:values=json.loads(f.read())try:my_cool_settings=MainSettings(values)exceptSettingErrorMessagease:sys.exit(e)settings_space=Space(my_cool_settings)print(f"Space dimensions:{settings_space.shape}")print(f"Space summary:{settings_space.cout_summary()}")print(f"Total number of elements:{len(settings_space)}")This will result in the following outputSpace dimensions: (3, 3, 3)
Space summary: Computational space dimensions: 3 x 3 x 3

axis: 0:
        primary_number
        values:  [1.0, 2.0, 3.0]
axis: 1:
        secondary_number
        values:  [4.0, 5.0, 6.0]
axis: 2:
        tertiary_number
        values:  [-1.0, -2.0, -3.0]

Total number of elements: 27We can couple two of the ranges together, such that the resultant space iterates
through matched parameters in step. Using the following JSON file{"primary_number":{"array":[1.0,2.0,3.0],"match":"best_match"},"secondary_number":{"min":4.0,"max":6.0,"num":3,},"tertiary_number":{"array":[-1.0,-2.0,-3.0],"match":"best_match"}}the resultant output isSpace dimensions: (3, 3)
Space summary: Computational space dimensions: 3 x 3

axis: 0:
        secondary_number
        values:  [4.0, 5.0, 6.0]

axis: 1:
        match_id: best_match
        primary_number
        tertiary_number
        values: [(1.0, -1.0), (2.0, -2.0), (3.0, -3.0)]
Total number of elements: 9We can see thatprimary_numberand `tertiary_number are coupled in order along
one axis.A few things to note:You can match an arbitary number of ranges together at different and arbitrary
depth.You can use any match string, and two ranges with the same match string will
be coupled.You can define an arbitrary number of match strings.String-Set-SettingsA common type of setting is a restricted set of string values. As suchjson_settingshas a special base classStringSetSetting. For example, we
might want to define a setting that is a type of vehicle{"vehicle_type":"car"}importsysimportjsonfromjson_settingsimportSettingsfromjson_settingsimportStringSetSettingfromjson_settingsimportSettingErrorMessageclassMainSettings(Settings):@Settings.assigndef__init__(self,values):self.vehicle_type=VehicleTypeSettingclassVehicleTypeSetting(StringSetSetting):@StringSetSetting.assigndef__init__(self,value):self.options=["car","plane","boat"]if__name__=="__main__":withopen("my_json_config_file.json",'r')asf:values=json.loads(f.read())try:my_cool_settings=MainSettings(values)exceptSettingErrorMessagease:sys.exit(e)A few things to note:StringSetSettingderived classes must have only one attributeoptionsdefined in the__init__method.optionsmust be alistofstrvalues.If a value that is not in the defined list is passed in the JSON file, the
following error message is yielded to the uservehicle_type -> must be one of ['car', 'plane', 'boat']List-SettingsWe want to define a setting that is a list of a single arbitrary type{"entries":[{"kind":"car","cost":1000},{"kind":"car","cost":3000},{"kind":"plane","cost":10000}]}importsysimportjsonfromjson_settingsimportSettingsfromjson_settingsimportListSettingfromjson_settingsimportStringSetSettingfromjson_settingsimportSettingErrorMessageclassMainSettings(Settings):@Settings.assigndef__init__(self,values):self.entries=EntryListSettingclassEntryListSetting(ListSetting):@ListSetting.assigndef__init__(self,values):self.type=EntrySettingclassEntrySetting(Settings):@Settings.assigndef__init__(self,values):self.kind=VehicleTypeSettingself.cost=intclassVehicleTypeSetting(StringSetSetting):@StringSetSetting.assigndef__init__(self,value):self.options=["car","plane","boat"]if__name__=="__main__":withopen("my_json_config_file.json",'r')asf:values=json.loads(f.read())try:my_cool_settings=MainSettings(values)exceptSettingErrorMessagease:sys.exit(e)print(f"Element type:{type(my_cool_settings[0])}")print(f"First element.kind:{my_cool_settings.entries[0].kind}")print(f"First element.cost:{my_cool_settings.entries[0].cost}")print(f"Third element.kind:{my_cool_settings.entries[2].kind}")print(f"Third element.cost:{my_cool_settings.entries[2].cost}")The above code will result in the following outputElement type: <class '__main__.EntrySetting'>
First element.kind: car
First element.cost: 1000
Third element.kind: plane
Third element.cost: 10000If we introduce an error in the configuration file{"entries":[{"kind":"caravan","cost":1000},{"kind":"car","cost":3000},{"kind":"plane","cost":10000}]}The following error message will be yielded to the user, noting the element of
the list where the error occurredentries[0] -> kind -> must be one of ['car', 'plane', 'boat']A few things to note:ListSettingsderived classes must define only one attributetypein the__init__method, which is the type of the values stored.A list can contain an arbitrary number of elements.All elements of the list must adhere to the constrants of all sub elements of
any settings objects contain within it.AnyListSettingderived object behaves like an immutablelist.List order from configuration files is maintained.Dictionary-SettingsWe want to define a setting that is a dictionary where all values are of a
single arbitrary type{"entries":{"cheap_car":{"kind":"car","cost":1000},"expensive_car":{"kind":"car","cost":3000},"cheap_plane":{"kind":"plane","cost":10000}}}importsysimportjsonfromjson_settingsimportSettingsfromjson_settingsimportDictionarySettingfromjson_settingsimportStringSetSettingfromjson_settingsimportSettingErrorMessageclassMainSettings(Settings):@Settings.assigndef__init__(self,values):self.entries=EntryListSettingclassEntryListSetting(DictionarySetting):@DictionarySetting.assigndef__init__(self,values):self.type=EntrySettingclassEntrySetting(Settings):@Settings.assigndef__init__(self,values):self.kind=VehicleTypeSettingself.cost=intclassVehicleTypeSetting(StringSetSetting):@StringSetSetting.assigndef__init__(self,value):self.options=["car","plane","boat"]if__name__=="__main__":withopen("my_json_config_file.json",'r')asf:values=json.loads(f.read())try:my_cool_settings=MainSettings(values)exceptSettingErrorMessagease:sys.exit(e)print(f"Element type:{type(my_cool_settings['cheap_car'])}")print(f"First element.kind:{my_cool_settings.entries['cheap_car'].kind}")print(f"First element.cost:{my_cool_settings.entries['cheap_car'].cost}")print(f"Third element.kind:{my_cool_settings.entries['cheap_plane'].kind}")print(f"Third element.cost:{my_cool_settings.entries['cheap_plane'].cost}")The above code will result in the following outputElement type: <class '__main__.EntrySetting'>
First element.kind: car
First element.cost: 1000
Third element.kind: plane
Third element.cost: 10000If we introduce an error in the configuration file{"entries":{"cheap_car":{"kind":"fish","cost":1000},"expensive_car":{"kind":"car","cost":3000},"cheap_plane":{"kind":"plane","cost":10000}}}The following error message will be yielded to the user, noting the key of
the dictionary where the error occurredentries -> cheap_car -> kind -> must be one of ['car', 'plane', 'boat']A few things to note:DictionarySettingsderived classes must define only one attributetypein the__init__method, which is the type of the values stored.A dictionary can contain an arbitrary number of key value pairs.All values of the dictionary must adhere to the constrants of all sub elements of
any settings objects contain within it.AnyDictionarySettingderived object behaves like an immutabledict.The key difference between this and a normalSettingsderived object is the
ability for the user to define arbitrary numbers of the same type of object to a
configuration file.