21
loading...
This website collects cookies to deliver better user experience
my_setting = config.value if config.value else some_default
and hated it, then I expect we can agree that handling application configuration is a miserable thing to code for. Its boring to write, ugly to read, and annoying to ensure that all user configuration options have sane default values.argparse
library can go a long way to making things better, but if you need to set dozens of options, or if your application supports plugins with configuration requirements unknown to the main program, it becomes harder to simply add_argument
our way back to sanity.constfig.py
and define the class _C
where the 'C' stands for both "constants" and "config" (🎶 and that's good enough for me. Both config and constants start with C 🎶). By convention, I lead the with an underscore to signal to the other developers on the project that the class isn't intended to be implemented directly, and at the end of the file I create an instance of _C()
called C
which can be imported and will contain all the information needed by the application._C
class, establish the variable names, pre-populate the variables with some reasonable default values, and then create an instance of our class named C
which the user is meant to import from our file.class _C(object):
def __init__(self):
self.LISTEN_IP = '0.0.0.0'
self.LISTEN_PORT = 8080
C = _C()
class _C(object):
def __init__(self):
# Constant values
self.JSON_RESPONSE_KEYWORD_D6ROLL = 'd6_roll'
# User configurable values
self.LISTEN_IP = '0.0.0.0'
self.LISTEN_PORT = 8080
C = _C()
C
(the instance of _C
) into our main Flask application.from constfig import C # Our constants + config = constfig
from random import randint
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/rolld6')
def roll_d6():
value = {C.JSON_RESPONSE_KEYWORD_D6ROLL: randint(1,6)}
return jsonify(value)
if __name__ == '__main__':
app.run(host=C.LISTEN_IP, port=C.LISTEN_PORT)
user@host:~ $ python3 roll.py
* Serving Flask app "roll" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
INFO:werkzeug: * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
user@host:~ $ curl -s localhost:8080/rolld6 | python3 -m json.tool
{
"d6_value": 3
}
_C
class, we're ready for it to poke at its own guts.load_config
which will open up our configuration file config.yaml
, and then use the setattr
function to update our own values at startup (when Python runs from constfig import C
). Python's ability to alter its own state is knows as "reflection" or "reflective programming".import yaml
import logging
class _C(object):
def __init__(self):
# Constant values - and a "gotcha!"
self.JSON_RESPONSE_KEYWORD_D6ROLL = 'd6_roll'
# User configurable values
self.LISTEN_IP = '0.0.0.0'
self.LISTEN_PORT = 8080
# Load user config (override defaults above)
self.load_config()
def load_config(self):
try:
config_file = open('config.yaml', 'r')
config_string = config_file.read()
config_file.close()
configuration = yaml.load(config_string, Loader=yaml.SafeLoader) # Don't handle badly formatted YAML. Let the parser inform the user of the error.
if isinstance(configuration, dict):
for variable_name, value in configuration.items():
setattr(self, variable_name, value)
else:
raise yaml.scanner.ScannerError(f'The file config.yaml should be structured as type dict, but got type {type(configuration)}')
except FileNotFoundError:
logging.warning('Configuration file config.yaml is missing. Using default values.')
C = _C()
config.yaml
file containing key:value pairs where the key matches the name of our configuration itemsLISTEN_IP: 127.0.0.1
LISTEN_PORT: 32000
user@host:~ $ python3 roll.py
* Serving Flask app "roll" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:32000/ (Press CTRL+C to quit)
JSON_RESPONSE_KEYWORD_D6ROLL: this is bad
in config.yaml
then make a request to our endpoint, we see that we've indirectly altered our application's response.user@host:~ $ curl -s http://127.0.0.1:32000/rolld6 | python3 -m json.tool
{
"this is bad": 4
}
finally
clause to our try/except block on load_config()
and call our new method set_constants()
.import yaml
import logging
class _C(object):
def __init__(self):
# User configurable values
self.LISTEN_IP = '0.0.0.0'
self.LISTEN_PORT = 8080
# Load user config (override defaults above)
self.load_config()
def set_constants(self):
# Constant values
self.JSON_RESPONSE_KEYWORD_D6ROLL = 'd6_value'
def load_config(self):
try:
config_file = open('config.yaml', 'r')
config_string = config_file.read()
config_file.close()
configuration = yaml.load(config_string, Loader=yaml.SafeLoader) # Don't handle badly formatted YAML. Let the parser inform the user of the error.
if isinstance(configuration, dict):
for variable_name, value in configuration.items():
setattr(self, variable_name, value)
else:
raise yaml.scanner.ScannerError(f'The file config.yaml should be structured as type dict, but got type {type(configuration)}')
except FileNotFoundError:
logging.warning('Configuration file config.yaml is missing. Using default values.')
finally:
self.set_constants()
C = _C()
JSON_RESPONSE_KEYWORD_D6ROLL
) configuration file.user@host:~ $ python3 roll.py
* Serving Flask app "roll" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:32000/ (Press CTRL+C to quit)
user@host:~ $ curl -s http://127.0.0.1:32000/rolld6 | python3 -m json.tool
{
"d6_value": 5
}
C
. To do this, let's define the function validate_config()
, write some basic assertions to validate the configuration, and then call the function after we have successfully loaded the yaml file and set the constant values in our class.import yaml
import logging
class _C(object):
def __init__(self):
# Default values for user configurable items
self.LISTEN_IP = '0.0.0.0'
self.LISTEN_PORT = 8080
# Load user config (override defaults above)
self.load_config()
def set_constants(self):
# Constant values
self.JSON_RESPONSE_KEYWORD_D6ROLL = 'd6_value'
def load_config(self):
try:
config_file = open('config.yaml', 'r')
config_string = config_file.read()
config_file.close()
configuration = yaml.load(config_string, Loader=yaml.SafeLoader) # Don't handle badly formatted YAML. Let the parser inform the user of the error.
if isinstance(configuration, dict):
for variable_name, value in configuration.items():
setattr(self, variable_name, value)
else:
raise yaml.scanner.ScannerError(f'The file config.yaml should be structured as type dict, but got type {type(configuration)}')
except FileNotFoundError:
logging.warning('Configuration file config.yaml is missing. Using default values.')
finally:
self.set_constants()
self.validate_config() # Validate our config file
def validate_config(self):
# Validate LISTEN_IP
assert isinstance(self.LISTEN_IP, str), 'LISTEN_IP is not a string value'
assert len(self.LISTEN_IP.split('.')) == 4, 'LISTEN_IP has an unexpected number of octets'
assert all([ip.isnumeric() for ip in self.LISTEN_IP.split('.')]), 'LISTEN_IP is not a valid IP address.'
assert all([0 <= int(ip) <= 255 for ip in self.LISTEN_IP.split('.')]), 'LISTEN_IP is not a valid IP address.'
# Validate LISTEN_PORT
if isinstance(self.LISTEN_PORT, str):
assert self.LISTEN_PORT.isnumeric(), 'LISTEN_PORT must be a whole number.'
self.LISTEN_PORT = int(C.LISTEN_PORT)
assert 999 < self.LISTEN_PORT < 65536, 'LISTEN_PORT is outside expected range.'
C = _C()
LISTEN_IP: 127.0.0.1x # My fingers are fat :-(
LISTEN_PORT: 32000
user@host:~ $ python3 roll.py
Traceback (most recent call last):
File "roll.py", line 1, in <module>
from constfig import C # Our constants + config = constfig
File "/Users/adrong/PycharmProjects/constfig/constfig.py", line 33, in <module>
validate_config()
File "/Users/adrong/PycharmProjects/constfig/constfig.py", line 14, in validate_config
assert all([a.isnumeric() for a in C.LISTEN_IP.split('.')]), 'LISTEN_IP is not a valid IP address.'
AssertionError: LISTEN_IP is not a valid IP address.
constfig.py
directly to validate your configuration in your test or deployment pipelines.user@host:~ $ python3 constfig.py
Traceback (most recent call last):
File "constfig.py", line 33, in <module>
validate_config()
File "constfig.py", line 14, in validate_config
assert all([a.isnumeric() for a in C.LISTEN_IP.split('.')]), 'LISTEN_IP is not a valid IP address.'
AssertionError: LISTEN_IP is not a valid IP address.