Package: carica
Carica - A Python ConfiguratorCarica is a python application configurator, interfacing between a pure python config module, and TOML representation of that module.CreditsA huge thank you goes to@sdispater, author of the fantastictomlkit library, which makes this project's variable docstrings retaining features possible.Project GoalsPython applications can be configured in a number of ways, each with its own advantages and limitations.Common Configuration MethodsMethodAdvantagesProblemsEnvironment variables/Command line argumentsEasy to handle in codeContainer/venv safeNot scalable to large numbers of variablesPrimative data types onlyNot human-friendlyNo typing in codeNo code autocompletion or other editor featuresDifficult to version controlTOML config fileContainer/venv safeMore scalableMore expressive, with tablesEasy to version controlHuman friendlyNot easy to manage in codeNo code autocompletion or other editor featuresNo dot syntax for objectsNo typing in codePython module with variablesEasy to handle in codeEasy to version control, with rich, human-readable diffsHighly scalableCompletely expressiveDot syntax for objectsVariable typing in codeComplete language and editor featuresNot container/venv safeNot human-friendlyModule must be accessible to the application namespace - difficult for packagesCarica aims to mix the best bits from two of the most convenient configuration methods, acting as an interface between pure python modules and TOML config files.Basic UsageTo use Carica, your application configuration should be defined as a python module.Example ApplicationloginApp.pyimportcfgimportsome_credentials_managerimportreprint(cfg.welcome_message)new_user_data={}forfield_name,field_configincfg.new_user_required_fields.items():print(field_config['display']+":")new_value=input()ifre.match(new_value,field_config['validation_regex']):new_user_data[field_name]=new_valueelse:raiseValueError(f"The value for{field_name}did not pass validation")some_credentials_manager.create_user(new_user_data)cfg.pywelcome_message="Welcome to the application. Please create an account:"new_user_required_fields={"username":{"display":"user-name","validation_regex":"[a-z]+"},"password":{"display":"pw","validation_regex":"\\b(?!password\\b)\\w+"},}Default config generationCarica is able to auto-generate a default TOML config file for your application, with the values specified in your python module as defaults:>>>importcfg>>>importcarica>>>carica.makeDefaultCfg(cfg)CreateddefaultCfg.tomlThe above code will produce the following file:defaultCfg.tomlwelcome_message="Welcome to the application. Please create an account:"[new_user_required_fields][new_user_required_fields.username]display="user-name"validation_regex="[a-z]+"[new_user_required_fields.password]display="pw"validation_regex="\\b(?!password\\b)\\w+"Loading a configuration fileCarica will map the variables given in your config file to those present in your python module.
Since the config python module contains default values, Carica does not require every variable to be specified:myConfig.toml[new_user_required_fields][new_user_required_fields.avatar]display="profile picture"validation_regex="[a-z]+">>>importcfg>>>importcarica>>>carica.loadCfg(cfg,"myConfig.toml")Configsuccessfullyloaded:myConfig.toml>>>importloginAppWelcometotheapplication.Pleasecreateanaccount:profilepicture:123Traceback(mostrecentcalllast):File"<stdin>",line1,in<module>File"loginApp.py",line14,in<module>raiseValueError(f"The value for{field_name}did not pass validation")ValueError:ThevalueforavatardidnotpassvalidationVariable Pseudo-DocstringsWhen encountering a comment in your python config module, Carica will treat it as a variable 'docstring' in the following cases:Inline comments on the same line as a variable declarationLine comments immediately preceeding a variable declaration ('preceeding comments')*Beta feature: still in testing*Line comments immediately preceeding an existing preceeding comment*Beta feature: still in testing*Carica will consider your variable docstrings when building TOML config files:cfg.py# This is shown to the user when the application is first launched# No validation is performed on this stringwelcome_message="Welcome to the application. Please create an account:"new_user_required_fields={# Each field should specify a 'display' (formatted field name shown to users) and a 'validation_regex', which inputted values will be checked against"username":{"display":"user-name","validation_regex":"[a-z]+"},"password":{"display":"pw","validation_regex":"\\b(?!password\\b)\\w+"},}>>>importcfg>>>importcarica>>>carica.makeDefaultCfg(cfg)CreateddefaultCfg.tomlThe above code will produce the following file:defaultCfg.toml# This is shown to the user when the application is first launched# No validation is performed on this stringwelcome_message="Welcome to the application. Please create an account:"[new_user_required_fields]# Each field should specify a 'display' (formatted field name shown to users) and a 'validation_regex', which inputted values will be checked against[new_user_required_fields.username]display="user-name"validation_regex="[a-z]+"[new_user_required_fields.password]display="pw"validation_regex="\\b(?!password\\b)\\w+"Advanced UsageCarica will handle non-primative variable types according to a very simple design pattern:TheSerializableTypetype protocolclassSerializableType:defserialize(self,**kwargs):...@classmethoddefdeserialize(cls,data,**kwargs):...Any type which definesserializeanddeserializemember methods will be automatically serialized during config generation, and deserialized on config loading.serializemust return a representation of your object with primative types - types which can be written to toml.deserializemust be a class method, and should transform a serialized object representation into a new object.Carica enforces this pattern on non-primative types using theSerializableTypetype protocol, which allows for duck-typed serializable types. This protocol is exposed for use withisinstance.Projects which prefer strong typing may implement thecarica.ISerializableinterface to enforce this pattern with inheritence. Carica will validate serialized objects against thecarica.PrimativeTypetype alias, which is also exposed for use.Examplecfg.pyclassMySerializableType:def__init__(self,myField):self.myField=myFielddefserialize(self,**kwargs):return{"myField":self.myField}@classmethoddefdeserialize(self,data,**kwargs):returnMySerializableClass(data["myField"])mySerializableVar=MySerializableClass("hello")Default config generation>>>importcfg>>>importcarica>>>carica.makeDefaultCfg(cfg)CreateddefaultCfg.tomlThe above code will produce the following file:defaultCfg.toml[mySerializableVar]myField="hello"Config file loadingmyConfig.toml[mySerializableVar]myField="some changed value">>>importcfg>>>importcarica>>>carica.loadCfg(cfg,"myConfig.toml")Configsuccessfullyloaded:myConfig.toml>>>cfg.mySerializableVar.myFieldsomechangedvaluePremade modelsCarica provides serializable models that are ready to use (or extend) in your code. These models can be found in thecarica.modelspackage, which is imported by default.SerializableDataClassRemoves the need to write boilerplate serializing functionality for dataclasses. This class is intended to be extended, adding definitions for your dataclass's fields. Extensions ofSerializableDataClassmustthemselves be decorated with@dataclasses.dataclassin order to function correctly.SerializablePathAn OS-agnostic filesystem path, extendingpathlib.Path. The serializing/deserializing behaviour added by this class is minimal, a serializedSerializablePathis simply the string representation of the path, for readability. All other behaviour ofpathlib.Pathapplies, for example.SerializablePathcan be instantiated from a single path:SerializablePath("my/directory/path"), or from path segments:SerializablePath("my", "file", "path.toml").SerializableTimedeltadatetime.datetimeis already considered a primitive type by TomlKit, and so no serializability needs to be added for you to use this class in your configs. However,datetime.timedeltais not serializable by default.SerializableTimedeltasolves this issue as a serializable subclass. As a subclass, alltimedeltabehaiour applies, including the usual constructor. In addition,SerializableTimedelta.fromTimedeltais a convenience class method that accepts adatetime.timedeltaand constructs a newSerializableTimedeltafrom it.Premade models exampleThe recommended usage pattern forSerializableDataClassis to separate your models into a separate module/package, allowing for 'schema' definition as python code. This pattern is not necessary, model definitioncanbe done in your config file.configSchema.pyfromcarica.modelsimportSerializableDataClassfromdataclassesimportdataclass@dataclassclassUserDataField(SerializableDataClass):name:strvalidation_regex:strconfig.pyfromcarica.modelsimportSerializablePath,SerializableTimedeltafromconfigSchemaimportUserDataFieldfromdatetimeimportdatetimenew_user_required_fields=[UserDataField(name="user-name"validation_regex="[a-z]+"),UserDataField(name="password"validation_regex="\\b(?!password\\b)\\w+")]database_path=SerializablePath("default/path.csv")birthday=datetime(day=1,month=1,year=1500)connection_timeout=SerializableTimedelta(minutes=5)Planned featuresPreceeding comments: This functionality is 'complete' in that it functions as intended and passes all unit tests, however an issue needs to be worked aruond before the feature can be enabled: In order to disambiguate between variables and table fields, the TOML spec requires that arrays and tables be placed at the end of a document. Carica currently depends upon documents being rendered with variables appearing in the same order as they appear in the python config module, which is not guaranteed. This leads to trailing and otherwise misplaced preceeding comments.Config mutation: Carica should allow for loading an existing config, changing some values, and then updating the TOML document with new values. This should retain all formatting from the original document, including variable ordering and any comments that are not present in the python module.LimitationsNo support for schema migrationNo support for asynchronous object serializing/deserializingImperfect estimation of variables defined in python modules: Listing the variables defined within a scope is not a known feature of python, and so Carica estimates this information by iterating over the tokens in your module. Carica does not build an AST of your python module.This means that certain name definition structures will result in false positives/negatives. This behaviour has not been extensively tested, but once such false positive has been identified:When invoking a callable (such as a class or function) with a keyword argument on a new, unindented line, the argument
name will be falsely identified as a variable name. E.g:my_variable=dict(key1=value1,key2=value2)producesmy_variableandkey2as variable names.