Package: ilexconf
Configuration Library 🔧 for Pythonilexconfis a Python library to load and merge configs from multiple sources, access & change the values, and write them back, if needed. It has no dependencies by default but provides additional functions, relying on popular libraries to parseyaml,toml, provideCLIapp, etc.📝 Full documentation atilexconf.comTable of contents🚀 Quick StartInstallationCreateConfigobjectRead usingfrom_functionsAccess valuesChange & create valuesmergeanotherMappinginto configConvert to simpledictusingas_dictSave to file usingto_functionsSubclassConfigto customize⚙️ Internals – How it WorksSubclassingdictand overriding methodsBuilding and hosting docs🚀 Quick StartInstall$pipinstallilexconfPopulate Config with valuesConfig object is initialized using arbitrary number of Mapping objects and keyword arguments. It can even be empty.fromilexconfimportConfig,from_json# All of these are valid methods to initialize a configconfig=Config()config=Config({"database":{"connection":{"host":"test.local"}}})config=Config(database__connection__port=4000)config=from_json("settings.json")config=from_env()# Or, you can combine themconfig=Config(# Take the basic settings from JSON filefrom_json("settings.json"),# Merge the dictionary into that{"database":{"connection":{"host":"test.local"}}},# Merge the keyword arguments on topdatabase__connection__port=4000)When we initialize config all the values are merged. Arguments are merged in order. Every next argument is merged on top of the previous mapping values. And keyword arguments override even that.For more details read aboutmergingstrategy below.For a settings filesettings.jsonwith the following content ...{"database":{"connection":{"host":"localhost","port":5432}}}The code above will produce a mergedconfigwith merged values:{"database":{"connection":{"host":"test.local","port":4000}}}Read from files & environment variablesFiles like.json,.yaml,.toml,.ini,.env,.pyas well as environment variables can all be read & loaded using a set offrom_functions.fromilexconfimport(from_json,# from JSON file or stringfrom_yaml,# from YAML file or stringfrom_toml,# from TOML file or stringfrom_ini,# from INI file or stringfrom_python,# from .py modulefrom_dotenv,# from .env filefrom_env# from environment variables)cfg1=from_json("settings.json")cfg2=Config(from_yaml("settings.yaml"),from_toml("settings.toml"))cfg3=Config(from_ini("settings.ini"),from_python("settings.py"),from_dotenv(".env"),from_env())Access values however you likeYou can access any key in the hierarchical structure using classical Python dict notation, dotted keys, attributes, or any combination of this methods.# Classic wayassertconfig["database"]["conection"]["host"]=="test.local"# Dotted keyassertconfig["database.connection.host"]=="test.local"# Attributesassertconfig.database.connection.host=="test.local"# Any combination of the aboveassertconfig["database"].connection.host=="test.local"assertconfig.database["connection.host"]=="test.local"assertconfig.database["connection"].host=="test.local"assertconfig.database.connection["host"]=="test.local"Change existing values and create new onesSimilarly, you can set values of any key (even if it doesn't exist in the Config) using all of the ways above.Notice,contrary to what you would expect from the Python dictionaries, setting nested keys that do not exist isallowed.# Classic wayconfig["database"]["connection"]["port"]=8080assertconfig["database"]["connection"]["port"]==8080# Dotted key (that does not exist yet)config["database.connection.user"]="root"assertconfig["database.connection.user"]=="root"# Attributes (also does not exist yet)config.database.connection.password="secret stuff"assertconfig.database.connection.password=="secret stuff"Merge with another Mapping objectIf you just assign a value to any key, you override any previous value of that key.In order to merge assigned value with an existing one, usemergemethod.config.database.connection.merge({"password":"different secret"})assertconfig.database.connection.password=="different secret"mergerespects the contents of each value. For example, merging two dictionaries with the same key would not override that key completely. Instead, it will recursively look into each key and try to merge the contents. Take this example:config=Config({"a1":{"c1":1,"c2":2,"c3":3}},{"a1":{"c3":"other"}})# Instead of overriding the value of the "a1" key completely, `merge` method# will recursively look inside and merge nested values.assertconfig.as_dict()=={"a1":{"c1":1,"c2":2,"c3":3}}Represent as dictionaryFor any purposes you might find fit you can convert entire structure of the Config object into dictionary, which will be essentially returned to you as a deep copy of the object.assertconfig.as_dict()=={"database":{"connection":{"host":"test.local","port":8080,"user":"root","password":"different secret"}}}Write to fileYou can serialize the file as JSON or other types any time using theto_functions.# Write updated config back as JSON filefromilexconfimportto_jsonto_json(config,"settings.json")WARNING:This might throw a serialization error if any of the values contained in the Config are custom objects that cannot be converted tostr. Also, obviously, you might not be able to correctly parse an object back, if it's saved to JSON asMyObject(<function MyObject.__init__.<locals>.<lambda> at 0x108927af0>, {})or something.SubclassSubclassingConfigclass is very convenient for implementation of your own config classes with custom logic.Consider this example:importilexconfclassConfig(ilexconf.Config):"""Your custom Configuration class"""def__init__(self,do_stuff=False):# Initialize your custom config with JSON by defaultsuper().__init__(self,ilexconf.from_json("setting.json"))# Add some custom value depending on some logicifdo_stuff:self.my.custom.key="Yes, do stuff"self.merge({"Horizon":"Up"})# Now you can use your custom Configuration everywhereconfig=Config(do_stuff=True)assertconfig.my.custom.key=="Yes, do stuff"assertconfig.Horizon=="Up"⚙️ InternalsImplementationUnder the hoodilexconfis implemented as adefaultdictwhere every key with Mapping value is represented as anotherConfigobject. This creates a hierarchy ofConfigobjects.__getitem__,__setitem__,__getattr__, and__setattr__methods are overloaded with custom logic to support convenient get/set approach presented by the library.DocumentationFull documentation is available atilexconf.comand on ilexconf.readthedocs.io.Documentation is written usingreStructuredTextand uses real code snippets from the unit tests and source code.Documentation is built usingSphinxusingsphinx-materialtheme.Documentation is hosted on Github Pages atilexconf.comand on "Read the Docs" atilexconf.readthedocs.io.For Github Pages documentation is built using Github Actions.Read the Docs builds their version automatically based on the.readthedocs.ymlconfig in the project root directory.ContributingContributions are welcome!Kudosilexconfideas are heavily borrowed from amazingpython-configurationlibrary byTiago Requeijo.LicenseMIT