classyconf¶
Configuration is just another API of your app. It allows us to preset or modify it’s behavior based on where it is installed and how it will be executed, providing more flexibility to the users of such software.
It is important to provide a clear separation of configuration and code. This is because config varies substantially across deploys and executions, code should not. The same code can be run inside a container or in a regular machine, it can be executed in production or in testing environments.
Configuration management is an important aspect of the architecture of any system. But it is sometimes overlooked.
Classyconf is here to help, it’s the configuration management solution for perfectionists with deadlines.
What’s classyconf¶
Classyconf is a framework agnostic python library created to make easy the separation of configuration and code.
It adds a declarative way to define settings for your projects contained in a class that can be extended, config objects can be passed around modules and settings are lazily loaded, plus some other goodies (aka dunder methods).
It’s classy, it’s pretty, it’s good.
Motivation¶
Configuration is just another API of your app, aimed for users who will install and run it, that allows them to preset the state of a program, without having to interact with it, only through static files or environment variables.
It is an important aspect of the architecture of any system, yet it is sometimes overlooked.
It is important to provide a clear separation of configuration and code. This is because config varies substantially across deploys and executions, code should not. The same code can be run inside a container or in a regular machine, it can be executed in production or in testing environments.
Settings discoverability¶
Well designed applications allow different ways to be configured. For example command line args are great to explore an app from the shell, but when you already know what you want, it would be great to set some defaults in a configuration file somewhere.
But what happens if a setting is passed as command line arg but also exist in the config file?
A proper settings-discoverability chain goes as follows:
- First command line args are checked.
- Then Environment variables.
- Config files in different directories, that also imply some hierarchy. For
example: config files in
/etc/myapp/settings.ini
are applied system-wide, while~/.config/myapp/settings.ini
take precedence and are user-specific. - Hardcoded constants as defaults.
Each one of this sources of configuration need to be properly collected and overwritten with an explicit level of hierarchy.
This raises the need to consolidate configuration in a single source of truth to avoid having config management scattered all over the codebase.
Parsing and casting¶
Not only each different source of configuration needs to be parsed differently but also each setting might need to be converted from a generic type like strings to proper types like integers or db connection structs.
Also each source of configuration follows some naming conventions, CLI args
look like this --flag=true
while environment variables can be FLAG=on
.
A settings architecture¶
Classyconf was born as a wrapper around prettyconf, inspired by goodconf, originally trying to follow the recomendations of 12 Factor’s topic about configs, but expanded to address all the cases stated above.
The good practices that this library suggest have an agnostic approach to configure applications, no matter if they are web, CLI or GUI apps, hosted on the cloud or running in your desktop.
Classyconf aims to be the settings management solution for perfectionists with deadlines.
Requirements¶
- Python 3.7+
Getting started¶
ClassyConf aims to be the configuration management solution for perfectionists with deadlines.
It solves many problems, so let’s get started with an incremental introduction to what it can do for you.
1. Declaring settings¶
The simplest ways to get started with classyconf is to use the
Configuration
class, to
declare all the settings of your app.
This pythonic class allows you to encapsulate all configuration
in one object by declaring your settings using the
Value
descriptor.
In this case, we will use a single debug setting, but there could be as many as you need.
from classyconf import Configuration, Value, as_boolean
class AppConfig(Configuration):
DEBUG = Value(default=False, cast=as_boolean, help="Toggle debugging on/off.")
We are using the as_boolean
cast for the DEBUG
setting. The
as_boolean
cast converts values like On|Off
, 1|0
, yes|no
,
true|false
, t|f
into Python boolean True
or False
.
See also
Visit the Casts section to find out more about other casts or how to write your own.
Since we provided a boolean default, there is no need to explicitely set
a cast in this case, classyconf will choose the as_boolean
to save
you some typing. Visit the Implicit casts section to see how to
customize this.
2. Discovering settings¶
Now that we defined the settings we needed, we will define where to obtain them.
Loaders will help you customize how configuration discovery works. This is a list of objects that will be used to discover settings from different sources.
Loaders can be declared in the Meta
class:
from classyconf import Configuration, Value, Environment
class AppConfig(Configuration):
DEBUG = Value(default=False, help="Toggle debugging on/off.")
class Meta:
loaders=[Environment()]
In this case we are telling classyconf to only search for settings in the
os.enviroment
variables. I know, this is not very useful, and seems like
an overkill. Let’s override this default loaders list and introduce another
loader to gather setting from a .ini file.
>>> from classyconf import Environment, IniFile
>>> config = AppConfig(loaders=[
... Environment(),
... IniFile("/etc/myapp/conf.ini", section="settings")
... ])
Now you might be asking, is this reading a file? do I have to create it? How do I access my settings?
Configuration discovery only happens when a Value
setting is first accessed,
so nothing gets evaluated until then.
The config instance can accessed as dict or object. Let’s trigger a look up:
>>> config.DEBUG # config["DEBUG"] also works!
False
Each loader is checked in the given order. In this case, we will first lookup
each setting in the os.enviroment
variables and, when not found, the
declared .ini file (inside the settings
section), but if this file
doesn’t exist or is broken, this loader is ignored.
If a setting is not found by any loader, the default value is returned, if
set, or a
UnknownConfiguration
exception is thrown.
Now we all know that the industry practices have set different naming
conventions for diffent configuration formats. Is it camelCase
for
.json files? Is it UPPER_CASE
for the enviroment variables and
lower_case
for .ini files? Don’t worry, classyconf has your back.
Most loaders include a keyfmt
callable argument. This allows you
to alter the name of the setting for each individual loader.
Let’s customize this:
>>> from classyconf import EnvPrefix
>>> config = AppConfig(loaders=[
... Environment(keyfmt=EnvPrefix("MY_APP_")),
... IniFile("/etc/myapp/conf.ini", section="settings", keyfmt=str.lower)
... ])
Now if you access config.DEBUG
, classyconf will first check for
MY_APP_DEBUG=xxx
in the os.enviroment
but for debug=xxx
in the
.ini
file.
See also
The rationale for keyfmt
is to follow the best practices for
naming variables, and respecting namespaces for each source of config.
Read more at Naming conventions and namespaces for settings.
3. Extending settings¶
As you know, the same code might run in several different enviroments, like dev, staging, prod, etc.
Although Configuration
classes can be extended to define new
Value
attributes or override
them, the recomended way is to simply override the settings sources per
enviroment.
from classyconf import EnvFile
class StagingConfig(AppConfig):
class Meta:
loaders = [EnvFile("staging.env")]
As we saw earlier, loaders can also be overridden at instantiation time.
from classyconf import Dict, EnvFile
test_config = AppConfig(loaders=[Dict({"DEBUG": True}), EnvFile("test.env")]
In the snippet above we used the Dict
loader, which comes handy to ensure certain hardcoded settings always get
picked up.
4. Inspecting settings¶
Later this object can be used to print configuration, this will evaluate every setting.
>>> config = AppConfig()
>>> print(config)
DEBUG=False - Toggle debugging on/off.
Or with __repl__()
you get a preview of how it was instantiated.
>>> config = AppConfig()
>>> config
AppConf(loaders=[Environment()])
It can also be iterated. This gives you the field key and the Value
instance for you to keep inspecting (this doesn’t evaluate the setting).
>>> for setting in config:
... print(setting)
...
('DEBUG', Value(key="DEBUG", help="Toggle debugging on/off."))
Configuration Loaders¶
The history of config files:
— David Capello (@davidcapello) May 19, 2020
.ini: maybe we need a little more
.xml: ok, this is too much
.json: ok, now we need comments back
.yaml: I'm not sure about this python approach
.toml: back to .ini
Loaders are in charge of loading configuration from various sources, like
.ini
files or environment variables, and expose configuration as a
dict-like object. Loaders are ment to be chained, so that classyconf checks
one by one for a given configuration variable.
If a loader doesn’t find the configuration variable it raises a KeyError
so that the next loader get’s checked. If no loader returns any value, and no
default value was set, an
UnknownConfiguration
exception is thrown.
Classyconf comes with some loaders already included in
classyconf.loaders
.
By default the library will check the environment with the
Environment
loader. You can
change that behaviour, by customizing the loaders and the order in wich
configuration discovery happens.
Loaders can be set in the Meta
class when extending
Configuration
or passed as a
param when instantiating it. The later takes precedence and overrides loaders
defined in Meta
. The order within the list of loaders matters and defines
the lookup order.
from classyconf import Configuration, IniFile, Environment, Value, EnvFile
class AppConfig(Configuration):
class Meta:
loaders = [
Environment(),
IniFile("/path/to/config.ini")
]
DEBUG = Value(default=False)
OTHER_CONFIG = Value(default=0)
# Checks for enviroment variables first
# Then lookup settings in the `config.ini` file
config = AppConfig()
# Override loaders and only lookup in the `.env` file
test_config = AppConfig(loaders=[EnvFile("/path/to/.env")])
Naming conventions and namespaces for settings¶
There happen to be some formatting conventions for configuration parameters based on where they are set. For example, it is common to name environment variables in uppercase:
$ DEBUG=yes OTHER_CONFIG=10 ./app.py
Since the environment is a global and shared dictionary, it is a good
practice to also apply some prefix to each setting to avoid collisions with
other known settings, like LOCALE
, TZ
, etc. This prefix works as a
namespace for your app.
$ MY_APP_DEBUG=yes MY_APP_OTHER_CONFIG=10 ./app.py
but if you were to set this config in an .ini
file, each setting should
probably be in lower case, the namespace is implicit in the file path, i.e:
/etc/myapp/config.ini
.
[settings]
debug=yes
other_config=10
Command line arguments have yet another conventions:
$ ./app.py --debug=yes --another-config=10
Classyconf let’s you follow these aesthetics patterns by setting a
keyfmt
function when instantiating the loaders.
By default, the Environment
is
instantiated with keyfmt=EnvPrefix('')
so that it looks for
UPPER_CASED settings. But it can be easyly tweaked to address the prefix
issue by using keyfmt=EnvPrefix("MY_APP_")
, and look for
MY_APP_UPPER_CASED to play nice with other env variables.
from classyconf import Configuration, IniFile, Environment, Value, EnvPrefix
class AppConfig(Configuration):
class Meta:
loaders = [
Environment(keyfmt=EnvPrefix(prefix="MY_APP_")),
IniFile("/etc/myapp/config.ini")
]
DEBUG = Value(default=False)
OTHER_CONFIG = Value(default=0)
config = AppConfig()
# looks for `MY_APP_DEBUG` in environment, then `debug` in `settings` section of config.ini
config.DEBUG
Keep reading to find out more about different loaders and their configurations.
Environment¶
-
class
classyconf.loaders.
Environment
(keyfmt=EnvPrefix(""))¶ Get’s configuration from the environment, by inspecting
os.environ
.Parameters: keyfmt (function) – A function to pre-format variable names.
The Environment
loader gets configuration from os.environ
. Since it
is a common pattern to write env variables in caps, the loader accepts a
keyfmt
function to pre-format the variable name before the lookup
occurs. By default it is EnvPrefix("")
which combines str.upper()
and an empty prefix.
Note
In the case of CLI apps, it would be recommended to set some sort of
namespace so that you don’t accidentally override other programs
behaviour, like LOCALE or EDITOR, but instead MY_APP_LOCALE, etc. So
consider using the EnvPrefix("MY_APP_")
approach.
from classyconf import Configuration, Environment, Value
class AppConf(Configuration):
debug = Value(default=False)
config = AppConf(loaders=[Environment(keyfmt=str.upper)])
config.debug # will look for a `DEBUG` variable
EnvFile¶
-
class
classyconf.loaders.
EnvFile
(filename='.env', keyfmt=EnvPrefix(""))¶ Parameters: - filename (str) – Path to the
.env
file. - keyfmt (function) – A function to pre-format variable names.
- filename (str) – Path to the
The EnvFile
loader gets configuration from .env
file. If the file
doesn’t exist, this loader will be skipped without raising any errors.
# .env file
DEBUG=1
from classyconf import Configuration, EnvFile, Value
class AppConf(Configuration):
debug = Value(default=False)
config = AppConf(loaders=[EnvFile(file='.env', keyfmt=str.upper)])
config.debug # will look for a `DEBUG` variable instead of `debug`
Note
You might want to use dump-env, a utility to create .env
files.
IniFile¶
-
class
classyconf.loaders.
IniFile
(filename, section='settings', keyfmt=<function IniFile.<lambda>>)¶ Parameters: - filename (str) – Path to the
.ini/.cfg
file. - section (str) – Section name inside the config file.
- keyfmt (function) – A function to pre-format variable names.
- filename (str) – Path to the
The IniFile
loader gets configuration from .ini
or .cfg
files. If
the file doesn’t exist, this loader will be skipped without raising any errors.
CommandLine¶
-
class
classyconf.loaders.
CommandLine
(parser, get_args=<function get_args>)¶ Extract configuration from an
argparse
parser.Parameters: - parser (argparse.ArgumentParser) – An argparse parser instance to extract variables from.
- get_args (function) – A function to extract args from the parser.
This loader lets you extract configuration variables from parsed command line arguments. By default it works with argparse parsers.
import argparse
from classyconf import Configuration, Value, NOT_SET, CommandLine
parser = argparse.ArgumentParser(description='Does something useful.')
parser.add_argument('--debug', '-d', dest='debug', default=NOT_SET, help='set debug mode')
class AppConf(Configuration):
DEBUG = Value(default=False)
config = AppConf(loaders=[CommandLine(parser=parser)])
print(config.DEBUG)
Something to notice here is the
NOT_SET
value. CLI parsers often
force you to put a default value so that they don’t fail. In that case, to
play nice with classyconf, you must set one. But that would break the
discoverability chain that classyconf encourages. So by setting this special
default value, you will allow classyconf to keep the lookup going.
The get_args
function converts the
argparse parser’s values to a dict that ignores
NOT_SET
values.
Dict¶
-
class
classyconf.loaders.
Dict
(values_mapping)¶ Parameters: values_mapping (dict) – A dictionary of hardcoded settings.
This loader is great when you want to pin certain settings without having to
change/override other loaders, files or defaults. It really comes handy when
you are extending a
Configuration
class.
from classyconf import Configuration, Value, IniFile, Dict
class AppConfig(Configuration):
class Meta:
loaders = [IniFile("/opt/myapp/config.ini"), IniFile("/etc/myapp/config.ini")]
NUMBER = Value(default=1)
DEBUG = Value(default=False)
LABEL = Value(default="foo")
OTHER = Value(default="bar")
class TestConfig(AppConfig):
class Meta:
loders = [Dict({"DEBUG": True, "NUMBER": 0})]
RecursiveSearch¶
-
class
classyconf.loaders.
RecursiveSearch
(starting_path=None, filetypes=(('.env', <class 'classyconf.loaders.EnvFile'>), (('*.ini', '*.cfg'), <class 'classyconf.loaders.IniFile'>)), root_path='/')¶ Parameters: - starting_path (str) – The path to begin looking for configuration files.
- filetypes (tuple) – tuple of tuples with configuration loaders, order matters.
Defaults to
(('*.env', EnvFile), (('*.ini', *.cfg',), IniFile)
- root_path (str) – Configuration lookup will stop at the given path. Defaults to the current user directory
This loader tries to find .env
or *.ini|*.cfg
files and load them
with the EnvFile
and
IniFile
loaders respectively.
It will start looking at the starting_path
directory for configuration
files and walking up the filesystem tree until it finds any or reaches the
root_path
.
Warning
It is important to note that this loader uses the glob module internally to
discover .env
and *.ini|*.cfg
files. This could be problematic if
the project includes many files that are unrelated, like a pytest.ini
file along side with a settings.ini
. An unexpected file could be found
and be considered as the configuration to use.
Consider the following file structure:
project/
settings.ini
app/
settings.py
When instantiating your
RecursiveSearch
, if you pass
/absolute/path/to/project/app/
as starting_path
the loader will start
looking for configuration files at project/app
.
# Code example in project/app/settings.py
import os
from classyconf import config
from classyconf.loaders import RecursiveSearch
app_path = os.path.dirname(__file__)
config.loaders = [RecursiveSearch(starting_path=app_path)]
By default, the loader will try to look for configuration files until it finds
valid configuration files or it reaches root_path
. The root_path
is
set to the root directory /
initialy.
Suppose the following file structure:
projects/
any_settings.ini
project/
app/
settings.py
You can change this behaviour by setting any parent directory of the
starting_path
as the root_path
when instantiating
RecursiveSearch
:
# Code example in project/app/settings.py
import os
from classyconf import Configuration
from classyconf.loaders import RecursiveSearch
app_path = os.path.dirname(__file__)
project_path = os.path.realpath(os.path.join(app_path, '..'))
rs = RecursiveSearch(starting_path=app_path, root_path=project_path)
config = Configuration(loaders=[rs])
The example above will start looking for files at project/app/
and will stop looking
for configuration files at project/
, actually never looking at any_settings.ini
and no configuration being loaded at all.
The root_path
must be a parent directory of starting_path
, otherwise
it raises an InvalidPath
exception:
from classyconf.loaders import RecursiveSearch
# /baz is not parent of /foo/bar, so this raises an InvalidPath exception here
rs = RecursiveSearch(starting_path="/foo/bar", root_path="/baz")
Writing your own loader¶
If you need a custom loader, you should just extend the
AbstractConfigurationLoader
.
-
class
classyconf.loaders.
AbstractConfigurationLoader
¶
For example, say you want to write a Yaml loader. It is important to note
that by raising a KeyError
exception from the loader, classyconf knows
that it has to keep looking down the loaders chain for a specific config.
import yaml
from classyconf.loaders import AbstractConfigurationLoader
class YamlFile(AbstractConfigurationLoader):
def __init__(self, filename, keyfmt=str.lower):
self.filename = filename
self.config = None
self.keyfmt = keyfmt
def _parse(self):
if self.config is not None:
return
with open(self.filename, 'r') as f:
self.config = yaml.load(f)
def __contains__(self, item):
try:
self._parse()
except:
return False
return self.keyfmt(item) in self.config
def __getitem__(self, item):
try:
self._parse()
except:
# KeyError tells classyconf to keep looking elsewhere!
raise KeyError("{!r}".format(item))
return self.config[self.keyfmt(item)]
def reset(self):
self.config = None
Then configure classyconf to use it.
from classyconf import Configuration
class AppConf(Configuration):
class Meta:
loaders = [YamlFile('/path/to/config.yml')]
Casts¶
Loaders gather configuration from different sources, but that configuration usually is digested as strings and it might not be the correct type you need in your programs.
That’s why you can specify cast functions for each individual setting.
from classyconf import Configuration, Value, Environment, as_boolean
from decimal import Decimal
class Config(Configuration)
class Meta:
loaders = [Environment()]
BASE_PRICE = Value(default=Decimal(10), cast=Decimal, help="Base product price.")
DEBUG = Value(default=False, cast=as_boolean, help="Enables debug mode.")
Buitin Casts¶
In classyconf.casts
you can find some common cast
functions that ship by default. If the cast fails it will rise an
InvalidConfiguration
exception.
Boolean¶
Converts values like On|Off
, 1|0
, yes|no
, y|n
,
true|false
, t|f
into booleans.
These options can be also extended by passing an extra True/False mapping.
from classyconf import Boolean
boolean = Boolean({"sim": True, "não": False})
assert boolean("sim")
assert boolean("yes")
assert not boolean("não")
assert not boolean("no")
List¶
Converts comma separated strings into lists by default.
This cast can accept other separators.
from classyconf import List
as_list = List(delimiter=";")
assert as_list("1; 2;3; ' 4; ';") == ['1', '2', '3', "' 4; '"]
Tuple¶
Same as List
, but converts comma separated strings into tuples.
from classyconf import Tuple
as_tuple = Tuple()
assert as_tuple("a, b, c") == ['a', 'b', 'c']
Option¶
Gets a return value based on specific options:
from classyconf import Option
choices = {
'option1': "asd",
'option2': "def",
}
option = Option(choices)
assert option("option1") == "asd"
assert option("option2") == "def"
Evaluate¶
Safely evaluate strings with Python literals to Python objects (alias to
Python’s ast.literal_eval
).
from classyconf import evaluate
assert evaluate("None") is None
Identity¶
It is the no-op type of cast, returns anything it receives as is.
from classyconf import Identity
as_is = Identity()
assert as_is("None") is "None"
Shortcuts for standard casts¶
classyconf
ships with cast instances already configured for convenience.
from classyconf import as_list, as_tuple, as_boolean, as_option, as_is, evaluate
They are pretty much self explanatory, but as_is
is an instance of
Identity
cast.
Custom casts¶
You can implement your own custom casting function by passing any callable:
from classyconf import Configuration, Environment
def number_list(value):
return [int(v) for v in value.split(";")]
class Config(Configuration)
class Meta:
loaders = [Environment()]
NUMBERS = Value(default="1;2;3", cast=number_list, help="Semicolon separated numbers.")
Implicit casts¶
classyconf
tries to provide some sensible default casts based on the
default’s value type.
- If the user provides a cast function, we use that one, no questions asked.
- If the user sets a default that is an
int
,str
,boolean
,float
, etc, and doesn’t set a cast function, we can set a default one:int()
,str()
,as_boolean()
andfloat()
respectively. - If the user doesn’t set a default value we use the Identity cast (
as_is()
). - If the user sets a non callable value as cast, we raise a
TypeError
exception.
So following the first example:
from classyconf import Configuration, Value, Environment
from decimal import Decimal
def number_list(value):
return [int(v) for v in value.split(";")]
class Config(Configuration)
class Meta:
loaders = [Environment()]
NUMBERS = Value("NUMBERS", default="1;2;3", cast=number_list) # cast is number_list
BASE_PRICE = Value(default=Decimal(10), help="Base product price.") # cast is Decimal
DEBUG = Value(default=False, help="Enables debug mode.") # cast is as_boolean
Advanced¶
Caching¶
Everytime you access a Value, classyconf peek on the loaders one by one until
a loader returns a setting. If a setting is not found by any loader, the
default value is returned, if set, or a
UnknownConfiguration
exception is thrown.
If the loaders chain is long or you are accessing the settings too often, there is an optimization you can use, which is the cache property:
from classyconf import Configuration, Value, Environment, EnvFile, IniFile
class AppConfig(Configuration):
DEBUG = Value(default=False, help="Toggle debugging on/off.")
class Meta:
loaders=[Environment(), EnvFile(".env"), IniFile("config.ini")]
cache = True
This property can also be set at runtime:
>>> config = AppConfig(cache=True)
>>>
It will make the lookup to have a O(1)
performance the second time it is
accesed.
Reloading new settings¶
Typically when files get parsed, their values are kept in an internal cache by each loader. If at some point you want to pickup new values, for example when using a long running daemon, call the reset method.
import signal
config = AppConfig()
def signal_handler(signum, frame):
if signum == signal.SIGHUP: # kill -1 <pid>
config.reset()
signal.signal(signal.SIGHUP, signal_handler)
if __name__ == '__main__':
main(config)
FAQ¶
Why not use environment variables directly?¶
There is a common pattern to read configurations in environment variable that look similar to the code below:
if os.environ.get("DEBUG", False):
print(True)
else:
print(False)
But this code have some issues:
- If envvar
DEBUG=False
this code will printTrue
becauseos.environ.get("DEBUG", False)
will return an string ‘False’ instead of a boolean False. And a non-empty string has aTrue
boolean value. - We can’t (dis|en)able debug with envvars
DEBUG=yes|no
,DEBUG=1|0
,DEBUG=True|False
. - If we want to use this configuration during development we need to define this envvar all the time. We can’t define this setting in a configuration file that will be used if DEBUG envvar is not defined.
When should I use configuration files?¶
Environment variables shouldn’t hold sensitive data, there are potential security issues:
- Accidental leaks via logging or error reporting services.
- Child process inheritance.
Command line arguments are great for exploring the possibilities of an app,
but passing lot’s of arguments either in the short -s
or long
--more-verbose
formats can be cumbersome.
Sometimes files are more convinient and documenting than command line arguments or env vars. Some file formats allow for comments and are great as templates to build upon.
If your app is a long running process, like a webserver, you can issue a
SIGHUP
signal so that it reloads it’s config from files. Env vars and
command line arguments cannot be easily changed from the outside after
startup.
Why are executable config files a bad idea?¶
Executable files can be used as config sources like .vimrc
,
Vagrantfile
, etc. This approach has some drawbacks.
First, your users now need to learn a new programming language, just to configure your application. Some apps (like the suckless bundle) go as far as requiring you to patch and compile your app to change it’s configuration.
And second, your configuration is no longer hierarchical, your application cannot extract configuration from different sources by executing different files, because you cannot know in advance what is being executed. So you typically end up with one single executable file as config that takes care of everything.
On the other hand, classyconf encourages traditional formats for configuration, like enviroment variables or ini files. The best way to think of configuration is as a set of key/value dicts that need to be merged into a single config dict. No need to get fancy.
Is classyconf tied to Django or Flask?¶
No, classyconf was designed to be framework agnostic, can be used for web, CLI or GUI applications.
Why create a library similar to prettyconf or goodconf instead of using it?¶
Although prettyconf is great and very flexible, I don’t like that the config(“debug”) call isn’t lazy, so putting it into a class isn’t enough:
from prettyconf import config
class MyConfig():
debug = config("debug") # this is evaluated when this module is loaded
I also didn’t like the default RecursiveSearch that it provides and I also needed to implement many changes and move fast to see what would work.
I’ve made several contributions to prettyconf and even have a talk about it, but I needed to change its behaviour, break things and move fast. This is backward incompatible, so, it could break software that relies on the old behaviour.
You can use any of them. Both are good libraries and provides a similar set of features.
Other libraries had other issues:
- Were tied to a specific web app framework.
- Didn’t allow you to specify configuration sources and their hierarchy.
- Had a global configuration object, or made it really hard to override specific configuration when writing tests.
- Settings were eagerly evaluated.
- Had no facilities for auto-generating configuration documentation or inspecting it.
Classyconf is classy, it’s pretty, it’s good.
How does classyconf compare to python-dotenv?¶
python-dotenv reads the key, value pair from .env file and adds them to environment variable. It is good for some tools that simply proxy the env to some other process, like docker-compose or pipenv.
On the other hand, classyconf does not populate the os.environ
dictionary,
because it is designed to discover configuration from diferent sources, the
environment being just one of them.
Other similar projects are direnv and envdir to load environment variables from directories and files.
In case you are running your app as an systemd unit, there is a section to directly list the env vars or to suply a env file.
What are some useful third-parties casts for Django?¶
Django is a popular python web framework that imposes some structure on the way its settings are configured. Here are a few 3rd party casts that help you adapt strings into that inner structures:
- dj-database-url - Parses URLs like
mysql://user:pass@server/db
into DjangoDATABASES
configuration format. - django-cache-url - Parses URLs like
memcached://server:port/prefix
into DjangoCACHES
configuration format. - dj-email-url - Parses URLs like
smtp://user@domain.com:pass@smtp.example.com:465/?ssl=True
with parameters used in DjangoEMAIL_*
configurations. - dj-admins-setting - Parses emails lists for the
ADMINS
configuration.
Changelog¶
All notable changes to this project will be documented in this file.
This project adheres to Semantic Versioning.
0.5.2¶
- Improved
pyproject.toml
metadata.
0.5.1¶
- Added python 3.9 support.
0.5.0¶
- Migrated from
setup.py
topyproject.toml
.- Refactored Makefile and added tbump directive.
0.4.0¶
- Changed references to the obsolete
ClassyConf
object in docs.- Exposed
CommandLine
loader in the library import root.
0.3.0¶
- Added keyword only flag for
Value
andConfiguration
classes.- Added cache option for
Configuration
class.
0.2.0¶
- Replaced
env_prefix
with theEnvPrefix
class.- Replaced coveralls with codecov.
- Replaced TravisCI with Github Actions.
0.1.0¶
- First version