first commit of bison work
This commit is contained in:
parent
00ac18b4b4
commit
2899d78659
|
@ -45,6 +45,7 @@ nosetests.xml
|
|||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
@ -99,3 +100,6 @@ ENV/
|
|||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# IDE
|
||||
.idea
|
|
@ -0,0 +1,49 @@
|
|||
#
|
||||
# bison - python application configuration
|
||||
#
|
||||
|
||||
HAS_PIPENV := $(shell which pipenv)
|
||||
PKG_VERSION := $(shell cat bison/__version__.py | grep __version__ | awk '{print $$3}')
|
||||
|
||||
requires-pipenv:
|
||||
ifndef HAS_PIPENV
|
||||
@echo "pipenv required, but not found: run 'pip install pipenv --upgrade'"
|
||||
exit 1
|
||||
endif
|
||||
|
||||
.PHONY: init
|
||||
init: requires-pipenv ## Initialize the project for development
|
||||
pipenv install --dev --skip-lock
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run the bison unit tests
|
||||
pipenv run py.test
|
||||
|
||||
.PHONY: ci
|
||||
ci: test lint ## Run the ci pipeline (test, lint)
|
||||
|
||||
.PHONY: lint
|
||||
lint: requires-pipenv ## Run static analysis / linting on bison
|
||||
pipenv run flake8 --ignore=E501,E712 bison
|
||||
pipenv run isort bison tests -rc -c --diff
|
||||
|
||||
.PHONY: coverage
|
||||
coverage: requires-pipenv ## Show the coverage report for unit tests
|
||||
pipenv run py.test --cov-report term --cov-report html --cov=bison tests
|
||||
|
||||
.PHONY: publish
|
||||
publish: ## Publish bison to Pypi
|
||||
pip install 'twine>=1.5.0'
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
rm -rf build dist .egg bison.egg-info
|
||||
|
||||
.PHONY: version
|
||||
version: ## Print the current version of the package
|
||||
@echo $(PKG_VERSION)
|
||||
|
||||
.PHONY: help
|
||||
help: ## Print Make usage information
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort
|
||||
|
||||
.DEFAULT_GOAL := help
|
|
@ -0,0 +1,18 @@
|
|||
[[source]]
|
||||
|
||||
url = "https://pypi.python.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
|
||||
[dev-packages]
|
||||
|
||||
"pytest" = ">=2.8.0"
|
||||
"pytest-cov" = "*"
|
||||
"isort" = "*"
|
||||
"flake8" = "*"
|
||||
"tox" = "*"
|
||||
|
||||
[packages]
|
||||
|
||||
"e1839a8" = {path = ".", editable = true}
|
|
@ -0,0 +1,192 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "fe0c46193a8684df9ffe0aef713c89d89219b59d1a89556e077620a673917c0b"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.python.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"e1839a8": {
|
||||
"editable": true,
|
||||
"path": "."
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:0c507b7f74b3d2dd4d1322ec8a94794927305ab4cebbe89cc47fe5e81541e6e8",
|
||||
"sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736",
|
||||
"sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f",
|
||||
"sha256:326420cbb492172dec84b0f65c80942de6cedb5233c413dd824483989c000608",
|
||||
"sha256:4474f8ea030b5127225b8894d626bb66c01cda098d47a2b0d3429b6700af9fd8",
|
||||
"sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab",
|
||||
"sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7",
|
||||
"sha256:5f84523c076ad14ff5e6c037fe1c89a7f73a3e04cf0377cb4d017014976433f3",
|
||||
"sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1",
|
||||
"sha256:b4c423ab23291d3945ac61346feeb9a0dc4184999ede5e7c43e1ffb975130ae6",
|
||||
"sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8",
|
||||
"sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4",
|
||||
"sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca",
|
||||
"sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269"
|
||||
],
|
||||
"version": "==3.12"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9",
|
||||
"sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450"
|
||||
],
|
||||
"version": "==17.4.0"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda",
|
||||
"sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"
|
||||
],
|
||||
"markers": "sys_platform == 'win32'",
|
||||
"version": "==0.3.9"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
"sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba",
|
||||
"sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed",
|
||||
"sha256:104ab3934abaf5be871a583541e8829d6c19ce7bde2923b2751e0d3ca44db60a",
|
||||
"sha256:15b111b6a0f46ee1a485414a52a7ad1d703bdf984e9ed3c288a4414d3871dcbd",
|
||||
"sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640",
|
||||
"sha256:1c383d2ef13ade2acc636556fd544dba6e14fa30755f26812f54300e401f98f2",
|
||||
"sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162",
|
||||
"sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508",
|
||||
"sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249",
|
||||
"sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694",
|
||||
"sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a",
|
||||
"sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287",
|
||||
"sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1",
|
||||
"sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000",
|
||||
"sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1",
|
||||
"sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e",
|
||||
"sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5",
|
||||
"sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062",
|
||||
"sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba",
|
||||
"sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc",
|
||||
"sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc",
|
||||
"sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99",
|
||||
"sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653",
|
||||
"sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c",
|
||||
"sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558",
|
||||
"sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f",
|
||||
"sha256:9e112fcbe0148a6fa4f0a02e8d58e94470fc6cb82a5481618fea901699bf34c4",
|
||||
"sha256:ac4fef68da01116a5c117eba4dd46f2e06847a497de5ed1d64bb99a5fda1ef91",
|
||||
"sha256:b8815995e050764c8610dbc82641807d196927c3dbed207f0a079833ffcf588d",
|
||||
"sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9",
|
||||
"sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd",
|
||||
"sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d",
|
||||
"sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6",
|
||||
"sha256:e4d96c07229f58cb686120f168276e434660e4358cc9cf3b0464210b04913e77",
|
||||
"sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80",
|
||||
"sha256:f8a923a85cb099422ad5a2e345fe877bbc89a8a8b23235824a93488150e45f6e"
|
||||
],
|
||||
"version": "==4.5.1"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
|
||||
"sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
|
||||
],
|
||||
"version": "==3.5.0"
|
||||
},
|
||||
"funcsigs": {
|
||||
"hashes": [
|
||||
"sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca",
|
||||
"sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"
|
||||
],
|
||||
"markers": "python_version < '3.0'",
|
||||
"version": "==1.0.2"
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
"sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af",
|
||||
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
|
||||
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
|
||||
],
|
||||
"version": "==4.3.4"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||
],
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff"
|
||||
],
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:8cca5c229d225f8c1e3085be4fcf306090b00850fefad892f9d96c7b6e2f310f",
|
||||
"sha256:ca18943e28235417756316bfada6cd96b23ce60dd532642690dcfdaba988a76d"
|
||||
],
|
||||
"version": "==1.5.2"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766",
|
||||
"sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"
|
||||
],
|
||||
"version": "==2.3.1"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
|
||||
"sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
|
||||
],
|
||||
"version": "==1.6.0"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:062027955bccbc04d2fcd5d79690947e018ba31abe4c90b2c6721abec734261b",
|
||||
"sha256:117bad36c1a787e1a8a659df35de53ba05f9f3398fb9e4ac17e80ad5903eb8c5"
|
||||
],
|
||||
"version": "==3.4.2"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
"sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d",
|
||||
"sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec"
|
||||
],
|
||||
"version": "==2.5.1"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
||||
],
|
||||
"version": "==1.11.0"
|
||||
},
|
||||
"tox": {
|
||||
"hashes": [
|
||||
"sha256:752f5ec561c6c08c5ecb167d3b20f4f4ffc158c0ab78855701a75f5cef05f4b8",
|
||||
"sha256:8af30fd835a11f3ff8e95176ccba5a4e60779df4d96a9dfefa1a1704af263225"
|
||||
],
|
||||
"version": "==2.9.1"
|
||||
},
|
||||
"virtualenv": {
|
||||
"hashes": [
|
||||
"sha256:02f8102c2436bb03b3ee6dede1919d1dac8a427541652e5ec95171ec8adbc93a",
|
||||
"sha256:39d88b533b422825d644087a21e78c45cf5af0ef7a99a1fc9fbb7b481e5c85b0"
|
||||
],
|
||||
"markers": "python_version != '3.2'",
|
||||
"version": "==15.1.0"
|
||||
}
|
||||
}
|
||||
}
|
214
README.md
214
README.md
|
@ -1,2 +1,212 @@
|
|||
# bison
|
||||
Python application configuration
|
||||
<p align="center">
|
||||
<a href="https://pypi.python.org/pypi/bison"><img src="https://img.shields.io/pypi/v/bison.svg"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/edaniszewski/bison.svg"></a>
|
||||
|
||||
<h1 align="center">bison</h1>
|
||||
</p>
|
||||
|
||||
<p align="center">Python application configuration</p>
|
||||
|
||||
|
||||
## What is Bison?
|
||||
Bison is a configuration solution for Python applications that aims to be simple
|
||||
and intuitive. It supports:
|
||||
* reading from YAML config files
|
||||
* reading from environment variables
|
||||
* setting explicit values
|
||||
* setting defaults
|
||||
* configuration validation
|
||||
* configuration access/manipulation with dot notation
|
||||
|
||||
Instead of implementing custom configuration reading and parsing, you can use
|
||||
Bison to handle it for you.
|
||||
|
||||
Bison was inspired by [Viper][viper] and the lack of good
|
||||
application configuration solutions for Python (at least, in my opinion). Documentation
|
||||
for Bison can be found on [ReadtheDocs][docs]
|
||||
|
||||
Bison uses the following precedence order. Each item in the list takes precedence
|
||||
over the item below it.
|
||||
- override (e.g. calling `Bison.set()`)
|
||||
- environment
|
||||
- config file
|
||||
- defaults
|
||||
|
||||
## Installation
|
||||
Bison can be installed with `pip`
|
||||
```
|
||||
pip install bison
|
||||
```
|
||||
or with `pipenv`
|
||||
```
|
||||
pipenv install bison
|
||||
```
|
||||
|
||||
## Using Bison
|
||||
### Creating a configuration Scheme
|
||||
A configuration scheme is not required by Bison, but having one allows you to set default
|
||||
values for configuration fields as well as do configuration validation. It is pretty easy
|
||||
to create a new Scheme:
|
||||
|
||||
```python
|
||||
scheme = bison.Scheme()
|
||||
```
|
||||
|
||||
A Scheme is really just a container for configuration options, so without any options, a
|
||||
Scheme is somewhat useless.
|
||||
|
||||
#### Configuration Options
|
||||
There are currently three types of configuration options:
|
||||
- `bison.Option`
|
||||
- `bison.DictOption`
|
||||
- `bison.ListOption`
|
||||
|
||||
Their intended functionality should be mostly obvious from their names. An `Option` represents
|
||||
a singular value in a configuration. A `DictOption` represents a dictionary or mapping of values
|
||||
in a configuration. A `ListOption` represents a list of values in a configuration.
|
||||
|
||||
See the [documentation][docs] for more on how options can be configured.
|
||||
|
||||
Any number of options can be added to a Scheme, but as a simple example we can define a Scheme
|
||||
which expects a key "log", and a key "count".
|
||||
|
||||
```python
|
||||
scheme = bison.Scheme(
|
||||
bison.Option('log'),
|
||||
bison.Option('count'),
|
||||
)
|
||||
```
|
||||
|
||||
#### Configuration Validation
|
||||
Validation operates based on the constraints set on the options. Above, there are no
|
||||
constraints (other than the need for those keys to exist), so any value for "log" and
|
||||
"count" will pass validation.
|
||||
|
||||
An option can be constrained in different ways by using its keyword arguments. For example,
|
||||
to ensure the value for "count" is an integer,
|
||||
```python
|
||||
bison.Option('count', field_type=int)
|
||||
```
|
||||
|
||||
Or, to restrict the values to a set of choices
|
||||
```python
|
||||
bison.Option('log', choices=['debug', 'info', 'warn', 'error'])
|
||||
```
|
||||
|
||||
The [documentation][docs] goes into more detail about other validation settings.
|
||||
|
||||
#### Setting Defaults
|
||||
If a default value is not set on an option, it is considered required. In these cases,
|
||||
if the key specified by that value is not present in the parse configuration, it will
|
||||
cause a validation failure.
|
||||
|
||||
If a default value is set, then the absence of that field in the configuration will not
|
||||
cause a validation failure.
|
||||
|
||||
```python
|
||||
bison.Option('log', default='info')
|
||||
```
|
||||
|
||||
### Configuring Bison
|
||||
Once you have a Scheme to use (if you'd like to), it will need to be passed to a Bison
|
||||
object to manage the config building.
|
||||
|
||||
```python
|
||||
scheme = bison.Scheme()
|
||||
config = bison.Bison(scheme)
|
||||
```
|
||||
|
||||
There are a few options that can be set on the Bison object to change how it
|
||||
searches for and builds the unified configuration.
|
||||
|
||||
For reading configuration files
|
||||
```python
|
||||
config.config_name = 'config' # name of the config file (no extension)
|
||||
config.add_config_paths( # paths to look in for the config file
|
||||
'.',
|
||||
'/tmp/app'
|
||||
)
|
||||
config.config_format = bison.YAML # the config format to use
|
||||
```
|
||||
|
||||
For reading environment variables
|
||||
```python
|
||||
config.env_prefix = "MY_APP" # the prefix to use for environment variables
|
||||
config.auto_env = True # automatically bind all options to env variables based on their key
|
||||
```
|
||||
|
||||
### Building the unified config
|
||||
Once the scheme has been set (if using) and Bison has been configured, the only thing
|
||||
left to do is to read in all the config sources and parse them into a unified config.
|
||||
This is done simply with
|
||||
```python
|
||||
config.parse()
|
||||
```
|
||||
|
||||
### Example
|
||||
Below is a complete example for parsing a hypothetical application configuration which
|
||||
is described by the following YAML config.
|
||||
```yaml
|
||||
log: debug
|
||||
port: 5000
|
||||
settings:
|
||||
requests:
|
||||
timeout: 3
|
||||
backends:
|
||||
- host: 10.1.2.3
|
||||
port: 5001
|
||||
- host: 10.1.2.4
|
||||
port: 5013
|
||||
- host: 10.1.2.5
|
||||
port: 5044
|
||||
```
|
||||
|
||||
```python
|
||||
import bison
|
||||
|
||||
# the scheme for the configuration. this allows us to set defaults
|
||||
# and validate configuration data
|
||||
config_scheme = bison.Scheme(
|
||||
bison.Option('log', default='info', choices=['debug', 'info', 'warn', 'error']),
|
||||
bison.Option('port', field_type=int),
|
||||
bison.DictOption('settings', scheme=bison.Scheme(
|
||||
bison.DictOption('requests', scheme=bison.Scheme(
|
||||
bison.Option('timeout', field_type=int)
|
||||
))
|
||||
)),
|
||||
bison.ListOption('backends', member_scheme=bison.Scheme(
|
||||
bison.Option('host', field_type=str),
|
||||
bison.Option('port', field_type=int)
|
||||
))
|
||||
)
|
||||
|
||||
# create a new Bison instance to store and manage configuration data
|
||||
config = bison.Bison(scheme=config_scheme)
|
||||
|
||||
# set the config file name to 'app' (default is 'config') and set the
|
||||
# search paths to '.' and '/tmp/app/config'
|
||||
config.config_name = 'app'
|
||||
config.add_config_paths('.', '/tmp/app/config')
|
||||
|
||||
# set the environment variable prefix and enable auto-env
|
||||
config.env_prefix = 'MY_APP'
|
||||
config.auto_env = True
|
||||
|
||||
# finally, parse the config sources to build the unified configuration
|
||||
config.parse()
|
||||
```
|
||||
|
||||
See the [example](example) directory for this example along with demonstrations
|
||||
of how to access configuration data.
|
||||
|
||||
## Future Work
|
||||
There is more that can be done to improve Bison and expand its functionality. If
|
||||
you wish to contribute, open a pull request. If you have questions or feature requests,
|
||||
open an issue. Below are some high level ideas for future improvements:
|
||||
|
||||
* Support additional configuration formats (JSON, TOML, ...)
|
||||
* Versioned configurations
|
||||
|
||||
|
||||
[docs]: http://readthedocs
|
||||
[viper]: https://github.com/spf13/viper
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
bison
|
||||
~~~~~
|
||||
|
||||
A Python application configuration library.
|
||||
"""
|
||||
# flake8: noqa
|
||||
|
||||
from .__version__ import __author__, __author_email__, __copyright__
|
||||
from .__version__ import __description__, __license__, __url__
|
||||
from .__version__ import __title__, __version__
|
||||
|
||||
from .bison import Bison
|
||||
from .scheme import DictOption, ListOption, Option, Scheme
|
||||
from .utils import DotDict
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
__title__ = 'bison'
|
||||
__description__ = 'Python application configuration'
|
||||
__url__ = 'https://github.com/edaniszewski/bison'
|
||||
__version__ = '0.0.1'
|
||||
__author__ = 'Erick Daniszewski'
|
||||
__author_email__ = 'edaniszewski@gmail.com'
|
||||
__license__ = 'MIT'
|
||||
__copyright__ = 'Copyright 2018 Erick Daniszewski'
|
|
@ -0,0 +1,261 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
bison.bison
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module implements the `bison` API.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import yaml
|
||||
|
||||
from bison.errors import BisonError
|
||||
from bison.scheme import Option
|
||||
from bison.utils import DotDict, cast
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
# enumerate the supported configuration formats
|
||||
YAML, = range(1)
|
||||
|
||||
|
||||
class Bison(object):
|
||||
"""The configuration management object.
|
||||
|
||||
The `Bison` object is used to search for, read, parse, and validate
|
||||
application configurations based on a set of defaults, configuration
|
||||
files, environment variables, and overrides. `Bison` is not intended
|
||||
to incorporate command line arguments.
|
||||
|
||||
By default, `Bison` is configured to look for a file named 'config'
|
||||
with the YAML format (file extension .yml or .yaml).
|
||||
|
||||
Args:
|
||||
scheme (bison.Scheme): The scheme to use for validation when parsing
|
||||
a configuration file. If no scheme is provided, no validation will
|
||||
occur. (default: None)
|
||||
enable_logging (bool): Enable `bison` logging. If this is set to True,
|
||||
`bison` will log out at the logging.INFO level; otherwise, logging
|
||||
for `bison` is disabled. (default: False)
|
||||
"""
|
||||
|
||||
# map the configuration format to its supported file extensions
|
||||
_fmt_to_ext = {
|
||||
YAML: ('.yml', '.yaml')
|
||||
}
|
||||
|
||||
# map the configuration format to the function is uses to load
|
||||
# the configuration data from file.
|
||||
_fmt_to_parser = {
|
||||
YAML: yaml.safe_load
|
||||
}
|
||||
|
||||
def __init__(self, scheme=None, enable_logging=False):
|
||||
logger.disabled = not enable_logging
|
||||
|
||||
self.scheme = scheme
|
||||
|
||||
self.config_name = 'config' # the name of the config file
|
||||
self.config_format = YAML # format of the config file
|
||||
self.config_paths = [] # the path(s) to search for the config file
|
||||
self.config_file = None # the config file to read
|
||||
|
||||
self.env_prefix = None # the environment variable prefix
|
||||
self.auto_env = False # automatically bind options to env variables
|
||||
|
||||
# the component configurations
|
||||
self._default = DotDict()
|
||||
self._config = DotDict()
|
||||
self._environment = DotDict()
|
||||
self._override = DotDict()
|
||||
|
||||
# the unified configuration.
|
||||
self._full_config = None
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
"""Get the complete configuration where the default, config,
|
||||
environment, and override values are merged together.
|
||||
|
||||
Returns:
|
||||
(DotDict): A dictionary of configuration values that
|
||||
allows lookups using dot notation.
|
||||
"""
|
||||
if self._full_config is None:
|
||||
self._full_config = DotDict()
|
||||
self._full_config.update(self._default)
|
||||
self._full_config.update(self._config)
|
||||
self._full_config.update(self._environment)
|
||||
self._full_config.update(self._override)
|
||||
return self._full_config
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""Get the value for the configuration `key`.
|
||||
|
||||
Args:
|
||||
key (str): The key into the configuration dictionary to get.
|
||||
default: The value to return if the given `key` is not found
|
||||
in the `Bison` config. This defaults to `None`.
|
||||
|
||||
Returns:
|
||||
The value for the given key, if it exists; `None` otherwise.
|
||||
"""
|
||||
return self.config.get(key, default)
|
||||
|
||||
def set(self, key, value):
|
||||
"""Set a value in the `Bison` configuration.
|
||||
|
||||
Args:
|
||||
key (str): The configuration key to set a new value for.
|
||||
value: The value to set.
|
||||
"""
|
||||
# the configuration changes, so we invalidate the cached config
|
||||
self._full_config = None
|
||||
self._override[key] = value
|
||||
|
||||
def add_config_paths(self, *paths):
|
||||
"""Add paths to search for the configuration file.
|
||||
|
||||
Args:
|
||||
*paths (str): The paths to search in for the configuration file.
|
||||
"""
|
||||
self.config_paths.extend(paths)
|
||||
|
||||
def validate(self):
|
||||
"""Validate the `Bison` configuration against the `Scheme`, if one
|
||||
is set.
|
||||
|
||||
Raises:
|
||||
errors.SchemeValidationError: The `Bison` configuration fails
|
||||
schema validation.
|
||||
"""
|
||||
if self.scheme:
|
||||
self.scheme.validate(self.config)
|
||||
|
||||
def parse(self):
|
||||
"""Parse the configuration sources into `Bison`."""
|
||||
self._parse_default()
|
||||
self._parse_config()
|
||||
self._parse_env()
|
||||
|
||||
def _find_config(self):
|
||||
"""Searches through the configured `config_paths` for the `config_name`
|
||||
file.
|
||||
|
||||
If there are no `config_paths` defined, this will raise an error, so the
|
||||
caller should take care to check the value of `config_paths` first.
|
||||
|
||||
Returns:
|
||||
str: The fully qualified path to the configuration that was found.
|
||||
|
||||
Raises:
|
||||
Exception: No paths are defined in `config_paths` or no file with
|
||||
the `config_name` was found in any of the specified `config_paths`.
|
||||
"""
|
||||
for search_path in self.config_paths:
|
||||
for ext in self._fmt_to_ext.get(self.config_format):
|
||||
path = os.path.abspath(os.path.join(search_path, self.config_name + ext))
|
||||
if os.path.isfile(path):
|
||||
self.config_file = path
|
||||
return
|
||||
raise BisonError('No file named {} found in search paths {}'.format(
|
||||
self.config_name, self.config_paths))
|
||||
|
||||
def _parse_config(self):
|
||||
"""Parse the configuration file, if one is configured, and add it to
|
||||
the `Bison` state.
|
||||
"""
|
||||
if len(self.config_paths) > 0:
|
||||
self._find_config()
|
||||
try:
|
||||
with open(self.config_file, 'r') as f:
|
||||
parsed = self._fmt_to_parser[self.config_format](f)
|
||||
except Exception as e:
|
||||
raise BisonError(
|
||||
'Failed to parse config file: {}'.format(self.config_file)
|
||||
) from e
|
||||
|
||||
# the configuration changes, so we invalidate the cached config
|
||||
self._full_config = None
|
||||
self._config = parsed
|
||||
|
||||
def _parse_env(self):
|
||||
"""Parse the environment variables for any configuration if an `env_prefix`
|
||||
is set.
|
||||
"""
|
||||
env_cfg = DotDict()
|
||||
|
||||
# if the env prefix doesn't end with '_', we'll append it here
|
||||
if self.env_prefix and not self.env_prefix.endswith('_'):
|
||||
self.env_prefix = self.env_prefix + '_'
|
||||
|
||||
# if there is no scheme, we won't know what to look for so only parse
|
||||
# config if there is a scheme.
|
||||
if self.scheme:
|
||||
for k, v in self.scheme.flatten().items():
|
||||
# only Options can be bound to env variables currently.
|
||||
if not isinstance(v, Option):
|
||||
continue
|
||||
|
||||
# we explicitly do not want to bind the option to env
|
||||
if v.bind_env is False:
|
||||
continue
|
||||
|
||||
# we want to bind the option to env. in this case, bind_env is
|
||||
# generated from the Option key.
|
||||
elif v.bind_env is True:
|
||||
env_key = k.replace('.', '_').upper()
|
||||
|
||||
# if an env prefix exists, use it
|
||||
if self.env_prefix:
|
||||
env_key = self.env_prefix.upper() + env_key
|
||||
|
||||
env = os.environ.get(env_key, None)
|
||||
if env is not None:
|
||||
env_cfg[k] = cast(v, env)
|
||||
|
||||
# bind_env holds the env variable to use. since it is specified
|
||||
# manually, we do not prepend the env prefix.
|
||||
elif isinstance(v.bind_env, str):
|
||||
env_key = v.bind_env
|
||||
|
||||
env = os.environ.get(env_key, None)
|
||||
if env is not None:
|
||||
env_cfg[k] = cast(v, env)
|
||||
|
||||
# bind_env is None - this is its default value. in this case, the
|
||||
# option hasn't been explicitly set as False, so we can do env
|
||||
# lookups if auto_env is set.
|
||||
elif v.bind_env is None:
|
||||
if self.auto_env:
|
||||
env_key = k.replace('.', '_').upper()
|
||||
|
||||
# if an env prefix exists, use it
|
||||
if self.env_prefix:
|
||||
env_key = self.env_prefix.upper() + env_key
|
||||
|
||||
env = os.environ.get(env_key, None)
|
||||
if env is not None:
|
||||
env_cfg[k] = cast(v, env)
|
||||
|
||||
if len(env_cfg) > 0:
|
||||
# the configuration changes, so we invalidate the cached config
|
||||
self._full_config = None
|
||||
self._environment.update(env_cfg)
|
||||
|
||||
def _parse_default(self):
|
||||
"""Parse the `Schema` for the `Bison` instance to create the set of
|
||||
default values.
|
||||
|
||||
If no defaults are specified in the `Schema`, the default dictionary
|
||||
will not contain anything.
|
||||
"""
|
||||
# the configuration changes, so we invalidate the cached config
|
||||
self._full_config = None
|
||||
|
||||
if self.scheme:
|
||||
self._default.update(self.scheme.build_defaults())
|
|
@ -0,0 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
bison.errors
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module defines the errors used by `bison`.
|
||||
"""
|
||||
|
||||
|
||||
class BisonError(Exception):
|
||||
"""The base class for all `bison` errors."""
|
||||
|
||||
|
||||
class InvalidSchemeError(BisonError):
|
||||
"""Error for when a `Scheme` contains incorrect members."""
|
||||
|
||||
|
||||
class SchemeValidationError(BisonError):
|
||||
"""An error for when a `bison` scheme fails validation."""
|
|
@ -0,0 +1,292 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
bison.scheme
|
||||
~~~~~~~~~~~~
|
||||
|
||||
This module defines the `Scheme`, which is used by `Bison`
|
||||
in order to do configuration defaults and validation. A
|
||||
`Scheme` is composed of various options, which are defined
|
||||
here as well.
|
||||
"""
|
||||
|
||||
from bison import errors
|
||||
|
||||
|
||||
class NoDefault:
|
||||
"""Defines a "no default" type.
|
||||
|
||||
This is used in place of `None` in an options' `default` field, since
|
||||
`None` is a valid default value, so checking the presence/absence of
|
||||
a default value cannot be a NoneType check.
|
||||
"""
|
||||
|
||||
|
||||
# global _NoDefault instance to use as the default value for options.
|
||||
_no_default = NoDefault()
|
||||
|
||||
|
||||
class Scheme(object):
|
||||
"""The `Scheme` specifies the expected options for a configuration.
|
||||
|
||||
It provides the template for what is expected when parsing and building
|
||||
configuration state. Additionally, it allows the user to specify default
|
||||
values for various fields. The `Scheme` allows for validation across all
|
||||
specified options, to the extent that the constraints are specified on
|
||||
those options.
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
self.args = args
|
||||
self._flat = None
|
||||
|
||||
def build_defaults(self):
|
||||
"""Build a dictionary of default values from the `Scheme`.
|
||||
|
||||
Returns:
|
||||
dict: The default configurations as set by the `Scheme`.
|
||||
|
||||
Raises:
|
||||
errors.InvalidSchemeError: The `Scheme` does not contain
|
||||
valid options.
|
||||
"""
|
||||
defaults = {}
|
||||
for arg in self.args:
|
||||
if not isinstance(arg, _BaseOpt):
|
||||
raise errors.InvalidSchemeError('')
|
||||
|
||||
# if there is a default set, add it to the defaults dict
|
||||
if not isinstance(arg.default, NoDefault):
|
||||
defaults[arg.name] = arg.default
|
||||
|
||||
# if we have a dict option, build the defaults for its scheme.
|
||||
# if any defaults exist, use them.
|
||||
if isinstance(arg, DictOption):
|
||||
b = arg.scheme.build_defaults()
|
||||
if b:
|
||||
defaults[arg.name] = b
|
||||
return defaults
|
||||
|
||||
def flatten(self):
|
||||
"""Flatten the scheme into a dictionary where the keys are
|
||||
compound 'dot' notation keys, and the values are the corresponding
|
||||
options.
|
||||
|
||||
Returns:
|
||||
dict: The flattened `Scheme`.
|
||||
"""
|
||||
if self._flat is None:
|
||||
flat = {}
|
||||
for arg in self.args:
|
||||
if isinstance(arg, Option):
|
||||
flat[arg.name] = arg
|
||||
|
||||
elif isinstance(arg, ListOption):
|
||||
flat[arg.name] = arg
|
||||
|
||||
elif isinstance(arg, DictOption):
|
||||
flat[arg.name] = DictOption
|
||||
for k, v in arg.scheme.flatten().items():
|
||||
flat[arg.name + '.' + k] = v
|
||||
|
||||
self._flat = flat
|
||||
return self._flat
|
||||
|
||||
def validate(self, config):
|
||||
"""Validate the given config against the `Schema`.
|
||||
|
||||
Args:
|
||||
config (dict): The configuration to validate.
|
||||
|
||||
Raises:
|
||||
errors.SchemeValidationError: The configuration fails
|
||||
validation against the `Schema`.
|
||||
"""
|
||||
if not isinstance(config, dict):
|
||||
raise errors.SchemeValidationError(
|
||||
'Scheme can only validate a dictionary config, but was given '
|
||||
'{} (type: {})'.format(config, type(config))
|
||||
)
|
||||
|
||||
for arg in self.args:
|
||||
# the option exists in the config
|
||||
if arg.name in config:
|
||||
arg.validate(config[arg.name])
|
||||
|
||||
# the option does not exist in the config
|
||||
else:
|
||||
# if the option has a default value, then it is
|
||||
# considered optional and is fine to omit, otherwise
|
||||
# it is considered to be required and its omission
|
||||
# is a validation error.
|
||||
if type(arg.default) == NoDefault:
|
||||
raise errors.SchemeValidationError(
|
||||
'Option "{}" is expected but not found.'.format(arg.name)
|
||||
)
|
||||
|
||||
|
||||
class _BaseOpt(object):
|
||||
"""Base class for all scheme options"""
|
||||
|
||||
def __init__(self):
|
||||
self.name = None
|
||||
self.default = NoDefault
|
||||
|
||||
def validate(self, value):
|
||||
"""Validate that the option constraints are met by the configuration.
|
||||
|
||||
Args:
|
||||
value: The value corresponding with the option.
|
||||
|
||||
Raises:
|
||||
errors.SchemeValidationError: The option failed validation.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Option(_BaseOpt):
|
||||
"""Option represents a configuration option with a singular value.
|
||||
|
||||
For YAML, this would be a single k:v pair, e.g.
|
||||
|
||||
debug: True
|
||||
|
||||
The above could be represented by the following:
|
||||
|
||||
Option('debug', field_type=bool)
|
||||
|
||||
Args:
|
||||
name (str): The name of the option - this should correspond to the key
|
||||
for the option in a configuration file, e.g.
|
||||
default: The default value to use. By default, this is the internal
|
||||
`_NoDefault` type. If the value of this is anything other than
|
||||
`_NoDefault`, this option is considered optional, so validation will
|
||||
not fail if it is not present in the config. If this is left at its
|
||||
default value of `_NoDefault`, then this option is considered required
|
||||
and will fail validation if not present.
|
||||
field_type: The type that the option value should have.
|
||||
choices (list|tuple): The valid options for the field.
|
||||
bind_env (bool|str|None): Bind the option to an environment variable.
|
||||
"""
|
||||
|
||||
def __init__(self, name, default=_no_default, field_type=None, choices=None, bind_env=None):
|
||||
super(Option, self).__init__()
|
||||
self.name = name
|
||||
self.default = default
|
||||
self.type = field_type
|
||||
self.choices = choices
|
||||
self.bind_env = bind_env
|
||||
|
||||
def validate(self, value):
|
||||
if (self.type is not None) and (type(value) != self.type):
|
||||
raise errors.SchemeValidationError(
|
||||
'{} is of type {}, but should be {}'.format(value, type(value), self.type)
|
||||
)
|
||||
if (self.choices is not None) and (value not in self.choices):
|
||||
raise errors.SchemeValidationError(
|
||||
'{} is not in the valid options: {}'.format(value, self.choices)
|
||||
)
|
||||
|
||||
|
||||
class DictOption(_BaseOpt):
|
||||
"""DictOption represents a configuration option with a dictionary value.
|
||||
|
||||
For YAML, this would be a k:v pair where the value is a map, e.g.
|
||||
|
||||
some_key:
|
||||
nested_key: value
|
||||
|
||||
The above could be represented by the following:
|
||||
|
||||
DictOption('some_key', scheme=Scheme(
|
||||
Option('nested_key')
|
||||
))
|
||||
|
||||
Args:
|
||||
name (str): The name of the option - this should correspond to the key
|
||||
for the option in a configuration file, e.g.
|
||||
scheme (Scheme|None): A Scheme that defines what the dictionary config should
|
||||
adhere to. This can be None if no validation is wanted on the option.
|
||||
default: The default value to use. By default, this is the internal
|
||||
`_NoDefault` type. If the value of this is anything other than
|
||||
`_NoDefault`, this option is considered optional, so validation will
|
||||
not fail if it is not present in the config. If this is left at its
|
||||
default value of `_NoDefault`, then this option is considered required
|
||||
and will fail validation if not present.
|
||||
bind_env (bool): Bind the option to an environment variable.
|
||||
"""
|
||||
|
||||
def __init__(self, name, scheme, default=_no_default, bind_env=False):
|
||||
super(DictOption, self).__init__()
|
||||
self.name = name
|
||||
self.default = default
|
||||
self.scheme = scheme
|
||||
self.bind_env = bind_env
|
||||
|
||||
def validate(self, value):
|
||||
if not isinstance(value, dict):
|
||||
raise errors.SchemeValidationError('{} is not a dictionary'.format(value))
|
||||
|
||||
if isinstance(self.scheme, Scheme):
|
||||
self.scheme.validate(value)
|
||||
|
||||
|
||||
class ListOption(_BaseOpt):
|
||||
"""ListOption represents a configuration option with a list value.
|
||||
|
||||
For YAML, this would be a k:v pair where the value is a list, e.g.
|
||||
|
||||
animals:
|
||||
- bison
|
||||
- buffalo
|
||||
|
||||
The above could be represented by the following:
|
||||
|
||||
ListOption('animals', member_type=str)
|
||||
|
||||
Args:
|
||||
name (str): The name of the option - this should correspond to the key
|
||||
for the option in a configuration file, e.g.
|
||||
default: The default value to use. By default, this is the internal
|
||||
`_NoDefault` type. If the value of this is anything other than
|
||||
`_NoDefault`, this option is considered optional, so validation will
|
||||
not fail if it is not present in the config. If this is left at its
|
||||
default value of `_NoDefault`, then this option is considered required
|
||||
and will fail validation if not present.
|
||||
member_type: The type that all members of the list should have.
|
||||
member_scheme (Scheme): The `Scheme` that all members of the list should
|
||||
fulfil. This should be used when the list members are dictionaries/maps.
|
||||
bind_env (bool): Bind the option to an environment variable.
|
||||
"""
|
||||
|
||||
def __init__(self, name, default=_no_default, member_type=None, member_scheme=None, bind_env=False):
|
||||
super(ListOption, self).__init__()
|
||||
self.name = name
|
||||
self.default = default
|
||||
self.member_type = member_type
|
||||
self.member_scheme = member_scheme
|
||||
self.bind_env = bind_env
|
||||
|
||||
def validate(self, value):
|
||||
if not isinstance(value, list):
|
||||
raise errors.SchemeValidationError('{} is not a list'.format(value))
|
||||
|
||||
if self.member_scheme is not None and self.member_type is not None:
|
||||
raise errors.SchemeValidationError(
|
||||
'Cannot specify both a member_type and a member_scheme.'
|
||||
)
|
||||
|
||||
if self.member_type is not None:
|
||||
for item in value:
|
||||
if type(item) != self.member_type:
|
||||
raise errors.SchemeValidationError(
|
||||
'Members in "{}" option are not of type {}'.format(self.name, self.member_type)
|
||||
)
|
||||
|
||||
if self.member_scheme is not None:
|
||||
if not isinstance(self.member_scheme, Scheme):
|
||||
raise errors.SchemeValidationError(
|
||||
'Specified member scheme is not an instance of a Scheme.'
|
||||
)
|
||||
|
||||
for item in value:
|
||||
self.member_scheme.validate(item)
|
|
@ -0,0 +1,206 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
bison.utils
|
||||
~~~~~~~~~~~
|
||||
|
||||
Utilities for `bison`.
|
||||
"""
|
||||
|
||||
from bison.errors import BisonError
|
||||
from bison.scheme import Option
|
||||
|
||||
|
||||
def cast(option, value):
|
||||
"""Cast a value to the type required by the option, if one is set.
|
||||
|
||||
This is used to cast the string values gathered from environment
|
||||
variable into their required type.
|
||||
|
||||
Args:
|
||||
option: The Option specifying the type.
|
||||
value: The value to cast.
|
||||
|
||||
Returns:
|
||||
The value casted to the expected type for the option.
|
||||
"""
|
||||
if not isinstance(option, Option):
|
||||
raise BisonError('Unable to cast - "{}" not an Option'.format(option))
|
||||
|
||||
# if there is no type set for the option, return the given
|
||||
# value unchanged.
|
||||
if option.type is None:
|
||||
return value
|
||||
|
||||
# cast directly
|
||||
if option.type in (str, int, float):
|
||||
try:
|
||||
return option.type(value)
|
||||
except Exception as e:
|
||||
raise BisonError(
|
||||
'Failed to cast {} to {}'.format(value, option.type)
|
||||
) from e
|
||||
|
||||
# for bool, can't cast a string, since a string is truthy,
|
||||
# so we need to check the value.
|
||||
elif option.type == bool:
|
||||
return value.lower() == 'true'
|
||||
|
||||
# the option type is currently not supported
|
||||
else:
|
||||
raise BisonError('Unsupported type for casting: {}'.format(option.type))
|
||||
|
||||
|
||||
def build_dot_value(key, value):
|
||||
"""Build new dictionaries based off of the dot notation key.
|
||||
|
||||
For example, if a key were 'x.y.z' and the value was 'foo',
|
||||
we would expect a return value of: ('x', {'y': {'z': 'foo'}})
|
||||
|
||||
Args:
|
||||
key (str): The key to build a dictionary off of.
|
||||
value: The value associated with the dot notation key.
|
||||
|
||||
Returns:
|
||||
tuple: A 2-tuple where the first element is the key of
|
||||
the outermost scope (e.g. left-most in the dot
|
||||
notation key) and the value is the constructed value
|
||||
for that key (e.g. a dictionary)
|
||||
"""
|
||||
# if there is no nesting in the key (as specified by the
|
||||
# presence of dot notation), then the key/value pair here
|
||||
# are the final key value pair.
|
||||
if key.count('.') == 0:
|
||||
return key, value
|
||||
|
||||
# otherwise, we will need to construct as many dictionaries
|
||||
# as there are dot components to hold the value.
|
||||
final_value = value
|
||||
reverse_split = key.split('.')[::-1]
|
||||
end = len(reverse_split) - 1
|
||||
for idx, k in enumerate(reverse_split):
|
||||
if idx == end:
|
||||
return k, final_value
|
||||
final_value = {k: final_value}
|
||||
|
||||
|
||||
class DotDict(dict):
|
||||
"""A dictionary which supports getting and setting with dot notation keys."""
|
||||
|
||||
def __init__(self, dct=None):
|
||||
if dct is None:
|
||||
dct = {}
|
||||
super(DotDict, self).__init__(dct)
|
||||
|
||||
def __getitem__(self, item):
|
||||
# x.__getitem__(y) <==> x[y], so this makes x[y] and x.get(y) go
|
||||
# through the same code path (e.g. __getattribute__)
|
||||
return self.get(item, None)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
# if there are no dots in the key, its a normal set
|
||||
if key.count('.') == 0:
|
||||
super(DotDict, self).__setitem__(key, value)
|
||||
return
|
||||
|
||||
# otherwise, traverse the key components to set the value
|
||||
first, remainder = key.split('.', 1)
|
||||
if first in self:
|
||||
v = super(DotDict, self).__getitem__(first)
|
||||
v.__setitem__(remainder, value)
|
||||
|
||||
else:
|
||||
k, v = build_dot_value(key, value)
|
||||
super(DotDict, self).__setitem__(k, v)
|
||||
|
||||
def __contains__(self, item):
|
||||
# if there are no dots in the key, it is a normal contains
|
||||
if item.count('.') == 0:
|
||||
return super(DotDict, self).__contains__(item)
|
||||
|
||||
# otherwise, traverse the key components to check contains
|
||||
first, remainder = item.split('.', 1)
|
||||
if first in self:
|
||||
v = super(DotDict, self).__getitem__(first)
|
||||
if isinstance(v, (dict, DotDict)):
|
||||
return DotDict(v).__contains__(remainder)
|
||||
return False
|
||||
return False
|
||||
|
||||
# ---------------------------------------
|
||||
# Public Facing Methods
|
||||
# ---------------------------------------
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""Get a value from the `DotDict`.
|
||||
|
||||
The `key` parameter can either be a regular string key,
|
||||
e.g. "foo", or it can be a string key with dot notation,
|
||||
e.g. "foo.bar.baz", to signify a nested lookup.
|
||||
|
||||
The default value is returned if any level of the key's
|
||||
components are not found.
|
||||
|
||||
Args:
|
||||
key (str): The key to get the value for.
|
||||
default: The return value should the given key
|
||||
not exist in the `DotDict`.
|
||||
"""
|
||||
# if there are no dots in the key, its a normal get
|
||||
if key.count('.') == 0:
|
||||
return super(DotDict, self).get(key, default)
|
||||
|
||||
# set the return value to the default
|
||||
value = default
|
||||
|
||||
# split the key into the first component and the rest of
|
||||
# the components. the first component corresponds to this
|
||||
# DotDict. the remainder components correspond to any nested
|
||||
# DotDicts.
|
||||
first, remainder = key.split('.', 1)
|
||||
if first in self:
|
||||
value = super(DotDict, self).get(first)
|
||||
|
||||
# if the value for the key at this level is a dictionary,
|
||||
# then pass the remainder to that DotDict.
|
||||
if isinstance(value, (dict, DotDict)):
|
||||
return DotDict(value).get(remainder)
|
||||
|
||||
# TODO: support lists
|
||||
|
||||
return value
|
||||
|
||||
def delete(self, key):
|
||||
"""Remove a value from the `DotDict`.
|
||||
|
||||
The `key` parameter can either be a regular string key,
|
||||
e.g. "foo", or it can be a string key with dot notation,
|
||||
e.g. "foo.bar.baz", to signify a nested element.
|
||||
|
||||
If the key does not exist in the `DotDict`, it will continue
|
||||
silently.
|
||||
|
||||
Args:
|
||||
key (str): The key to remove.
|
||||
"""
|
||||
dct = self
|
||||
keys = key.split('.')
|
||||
last_key = keys[-1]
|
||||
for k in keys:
|
||||
# if the key is the last one, e.g. 'z' in 'x.y.z', try
|
||||
# to delete it from its dict.
|
||||
if k == last_key:
|
||||
del dct[k]
|
||||
break
|
||||
|
||||
# if the dct is a DotDict, get the value for the key `k` from it.
|
||||
if isinstance(dct, DotDict):
|
||||
dct = super(DotDict, dct).__getitem__(k)
|
||||
|
||||
# otherwise, just get the value from the default __getitem__
|
||||
# implementation.
|
||||
else:
|
||||
dct = dct.__getitem__(k)
|
||||
if not isinstance(dct, (DotDict, dict)):
|
||||
raise KeyError(
|
||||
'Subkey "{}" in "{}" invalid for deletion'.format(k, key)
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
log: debug
|
||||
port: 5000
|
||||
settings:
|
||||
requests:
|
||||
timeout: 3
|
||||
backends:
|
||||
- host: 10.1.2.3
|
||||
port: 5001
|
||||
- host: 10.1.2.4
|
||||
port: 5013
|
||||
- host: 10.1.2.5
|
||||
port: 5044
|
|
@ -0,0 +1,52 @@
|
|||
"""
|
||||
A (contrived) example of how to use bison
|
||||
"""
|
||||
|
||||
import bison
|
||||
|
||||
# the scheme for the configuration. this allows us to set defaults
|
||||
# and validate configuration data
|
||||
config_scheme = bison.Scheme(
|
||||
bison.Option('log', default='info', choices=['debug', 'info', 'warn', 'error']),
|
||||
bison.Option('port', field_type=int),
|
||||
bison.DictOption('settings', scheme=bison.Scheme(
|
||||
bison.DictOption('requests', scheme=bison.Scheme(
|
||||
bison.Option('timeout', field_type=int)
|
||||
))
|
||||
)),
|
||||
bison.ListOption('backends', member_scheme=bison.Scheme(
|
||||
bison.Option('host', field_type=str),
|
||||
bison.Option('port', field_type=int)
|
||||
))
|
||||
)
|
||||
|
||||
# create a new Bison instance to store and manage configuration data
|
||||
config = bison.Bison(scheme=config_scheme)
|
||||
|
||||
# set the config file name to 'app' (default is 'config') and set the
|
||||
# search paths to '.' and '/tmp/app/config'
|
||||
config.config_name = 'app'
|
||||
config.add_config_paths('.', '/tmp/app/config')
|
||||
|
||||
# set the environment variable prefix and enable auto-env
|
||||
config.env_prefix = 'MY_APP'
|
||||
config.auto_env = True
|
||||
|
||||
# finally, parse the config sources to build the unified configuration
|
||||
config.parse()
|
||||
|
||||
|
||||
# now, for examples sake, lets see how data can be accessed
|
||||
|
||||
print('Unified configuration: {}'.format(config.config))
|
||||
|
||||
print('Getting values:')
|
||||
print('\tlog: {}'.format(config.get('log')))
|
||||
print('\tport: {}'.format(config.get('port')))
|
||||
print('\tsettings: {}'.format(config.get('settings')))
|
||||
print('\tsettings.requests.timeout: {}'.format(config.get('settings.requests.timeout')))
|
||||
|
||||
print('Setting values:')
|
||||
print('\tlog (pre): {}'.format(config.get('log')))
|
||||
config.set('log', 'warn')
|
||||
print('\tlog (post): {}'.format(config.get('log')))
|
|
@ -0,0 +1,5 @@
|
|||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[metadata]
|
||||
license_file = LICENSE
|
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
# Load the package's __version__.py module as a dictionary.
|
||||
pkg = {}
|
||||
with open(os.path.join(here, 'bison', '__version__.py')) as f:
|
||||
exec(f.read(), pkg)
|
||||
|
||||
# Load the README
|
||||
with open('README.md', 'r') as f:
|
||||
readme = f.read()
|
||||
|
||||
|
||||
setup(
|
||||
name=pkg['__title__'],
|
||||
version=pkg['__version__'],
|
||||
description=pkg['__description__'],
|
||||
long_description=readme,
|
||||
author=pkg['__author__'],
|
||||
author_email=pkg['__author_email__'],
|
||||
url=pkg['__url__'],
|
||||
license=pkg['__license__'],
|
||||
packages=find_packages(),
|
||||
package_data={'': ['LICENSE']},
|
||||
package_dir={'bison': 'bison'},
|
||||
include_package_data=True,
|
||||
python_requires=">=3.4",
|
||||
install_requires=[
|
||||
'pyyaml'
|
||||
],
|
||||
zip_safe=False,
|
||||
classifiers=[
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Natural Language :: English',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: Implementation :: CPython',
|
||||
'Programming Language :: Python :: Implementation :: PyPy',
|
||||
'Topic :: Software Development :: Libraries',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules'
|
||||
],
|
||||
tests_require=[
|
||||
'pytest>=2.8.0',
|
||||
'pytest-cov'
|
||||
]
|
||||
)
|
|
@ -0,0 +1,52 @@
|
|||
"""Test fixtures for Bison unit tests."""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def yaml_config(tmpdir):
|
||||
"""Create a YAML config file."""
|
||||
cfg = tmpdir.join('config.yml')
|
||||
cfg.write("""
|
||||
foo: True
|
||||
bar:
|
||||
baz: 1
|
||||
test: value
|
||||
""")
|
||||
|
||||
yield cfg
|
||||
|
||||
cfg.remove()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def bad_yaml_config(tmpdir):
|
||||
"""Create a bad YAML config file."""
|
||||
cfg = tmpdir.join('config.yml')
|
||||
cfg.write("""
|
||||
field:\n::>:!~-:
|
||||
""")
|
||||
|
||||
yield cfg
|
||||
|
||||
cfg.remove()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def with_env():
|
||||
"""Set and cleanup environment variables for tests."""
|
||||
os.environ['TEST_ENV_FOO'] = 'bar'
|
||||
os.environ['TEST_ENV_NESTED_ENV_KEY'] = 'test'
|
||||
os.environ['TEST_OTHER_ENV_BAR'] = 'baz'
|
||||
os.environ['FOO_INT'] = '1'
|
||||
os.environ['FOO_BOOL'] = 'False'
|
||||
|
||||
yield
|
||||
|
||||
del os.environ['TEST_ENV_FOO']
|
||||
del os.environ['TEST_ENV_NESTED_ENV_KEY']
|
||||
del os.environ['TEST_OTHER_ENV_BAR']
|
||||
del os.environ['FOO_INT']
|
||||
del os.environ['FOO_BOOL']
|
|
@ -0,0 +1,466 @@
|
|||
"""Unit tests for bison.bison"""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
import bison
|
||||
from bison import errors
|
||||
from bison.bison import YAML
|
||||
|
||||
|
||||
class TestBison:
|
||||
"""Test for the `Bison` class."""
|
||||
|
||||
def test_simple_init(self):
|
||||
"""Initialize a Bison object."""
|
||||
b = bison.Bison()
|
||||
|
||||
assert b.scheme is None
|
||||
assert b.config_name == 'config'
|
||||
assert b.config_paths == []
|
||||
assert b.config_file is None
|
||||
assert b.env_prefix is None
|
||||
assert b.auto_env is False
|
||||
|
||||
assert b._full_config is None
|
||||
assert isinstance(b._default, bison.DotDict)
|
||||
assert isinstance(b._config, bison.DotDict)
|
||||
assert isinstance(b._environment, bison.DotDict)
|
||||
assert isinstance(b._override, bison.DotDict)
|
||||
|
||||
assert len(b._default) == 0
|
||||
assert len(b._config) == 0
|
||||
assert len(b._environment) == 0
|
||||
assert len(b._override) == 0
|
||||
|
||||
def test_config_property_empty(self):
|
||||
"""Get the full configuration when nothing is set."""
|
||||
b = bison.Bison()
|
||||
|
||||
assert b._full_config is None
|
||||
c = b.config
|
||||
assert isinstance(c, bison.DotDict)
|
||||
assert len(c) == 0
|
||||
assert b._full_config == c
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'key,expected,config', [
|
||||
('foo', None, None),
|
||||
('foo', None, bison.DotDict()),
|
||||
('foo', None, bison.DotDict({'foo': None})),
|
||||
('foo', 'bar', bison.DotDict({'foo': 'bar'})),
|
||||
('foo.bar', 'baz', bison.DotDict({'foo': {'bar': 'baz'}})),
|
||||
('foo.bar.baz', 1, bison.DotDict({'foo': {'bar': {'baz': 1}}})),
|
||||
]
|
||||
)
|
||||
def test_get(self, key, expected, config):
|
||||
"""Get configuration values from Bison."""
|
||||
b = bison.Bison()
|
||||
b._full_config = config # for the test, set the config manually
|
||||
|
||||
value = b.get(key)
|
||||
assert value == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'key,value', [
|
||||
('foo', 'bar'),
|
||||
('foo', 1),
|
||||
('foo', False),
|
||||
('foo', True),
|
||||
('foo', None),
|
||||
('foo.bar', 'baz'),
|
||||
('foo.bar.baz', 1),
|
||||
]
|
||||
)
|
||||
def test_set(self, key, value):
|
||||
"""Set configuration overrides for a Bison instance."""
|
||||
b = bison.Bison()
|
||||
assert len(b._override) == 0
|
||||
assert b.get(key) is None
|
||||
|
||||
b.set(key, value)
|
||||
|
||||
assert len(b._override) == 1
|
||||
assert b.get(key) == value
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'paths', [
|
||||
(),
|
||||
('path1',),
|
||||
('path1', 'path2'),
|
||||
('path1', 'path2', 'path3')
|
||||
]
|
||||
)
|
||||
def test_add_config_paths(self, paths):
|
||||
"""Add configuration paths to the Bison instance."""
|
||||
b = bison.Bison()
|
||||
assert len(b.config_paths) == 0
|
||||
|
||||
b.add_config_paths(*paths)
|
||||
assert len(b.config_paths) == len(paths)
|
||||
|
||||
def test_validate_no_scheme(self):
|
||||
"""Validate the Bison configuration when there is no Scheme to validate against."""
|
||||
b = bison.Bison()
|
||||
b.set('foo', 'bar')
|
||||
b.validate()
|
||||
|
||||
def test_validate_ok(self):
|
||||
"""Validate the Bison configuration successfully."""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', field_type=str)
|
||||
))
|
||||
|
||||
# add 'foo' to the config
|
||||
b.set('foo', 'bar')
|
||||
|
||||
# validation should succeed -- the value of 'foo' is a string
|
||||
b.validate()
|
||||
|
||||
def test_validate_fail(self):
|
||||
"""Validate the Bison configuration unsuccessfully."""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', field_type=str)
|
||||
))
|
||||
|
||||
# add 'foo' to the config
|
||||
b.set('foo', 1)
|
||||
|
||||
# validation should fail -- the value of 'foo' is not a string
|
||||
with pytest.raises(errors.SchemeValidationError):
|
||||
b.validate()
|
||||
|
||||
def test_parse_no_sources(self):
|
||||
"""Parse when there are no sources to parse from."""
|
||||
b = bison.Bison()
|
||||
b.parse()
|
||||
assert len(b.config) == 0
|
||||
|
||||
def test_find_config(self, yaml_config):
|
||||
"""Find a config file when it does exist"""
|
||||
b = bison.Bison()
|
||||
b.config_name = 'config'
|
||||
b.config_format = YAML
|
||||
b.config_paths = ['.', yaml_config.dirname]
|
||||
|
||||
assert b.config_file is None
|
||||
|
||||
b._find_config()
|
||||
|
||||
assert b.config_file == os.path.join(yaml_config.dirname, yaml_config.basename)
|
||||
|
||||
def test_find_config_nonexistent(self):
|
||||
"""Find a config file when it does not exist"""
|
||||
b = bison.Bison()
|
||||
b.config_name = 'config'
|
||||
b.config_format = YAML
|
||||
b.config_paths = ['.']
|
||||
|
||||
assert b.config_file is None
|
||||
|
||||
with pytest.raises(errors.BisonError):
|
||||
b._find_config()
|
||||
|
||||
def test_parse_config_no_paths(self):
|
||||
"""Parse the file config when no paths are specified"""
|
||||
b = bison.Bison()
|
||||
|
||||
assert b.config_file is None
|
||||
assert len(b._config) == 0
|
||||
|
||||
b._parse_config()
|
||||
|
||||
assert b.config_file is None
|
||||
assert len(b._config) == 0
|
||||
|
||||
def test_parse_config_ok(self, yaml_config):
|
||||
"""Parse the file config successfully."""
|
||||
b = bison.Bison()
|
||||
b.add_config_paths(yaml_config.dirname)
|
||||
|
||||
assert b.config_file is None
|
||||
assert len(b._config) == 0
|
||||
|
||||
b._parse_config()
|
||||
|
||||
assert b.config_file == os.path.join(yaml_config.dirname, yaml_config.basename)
|
||||
assert len(b._config) == 2
|
||||
|
||||
def test_parse_config_fail(self, bad_yaml_config):
|
||||
"""Parse the file config unsuccessfully."""
|
||||
b = bison.Bison()
|
||||
b.add_config_paths(bad_yaml_config.dirname)
|
||||
|
||||
assert b.config_file is None
|
||||
assert len(b._config) == 0
|
||||
|
||||
with pytest.raises(errors.BisonError):
|
||||
b._parse_config()
|
||||
|
||||
assert b.config_file == os.path.join(bad_yaml_config.dirname, bad_yaml_config.basename)
|
||||
assert len(b._config) == 0
|
||||
|
||||
def test_parse_defaults_no_scheme(self):
|
||||
"""Parse the defaults when there is no Scheme."""
|
||||
b = bison.Bison()
|
||||
|
||||
assert len(b._default) == 0
|
||||
assert b._full_config is None
|
||||
|
||||
b._parse_default()
|
||||
|
||||
assert len(b._default) == 0
|
||||
assert b._full_config is None
|
||||
|
||||
def test_parse_defaults_ok(self):
|
||||
"""Parse the defaults successfully."""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', default='bar')
|
||||
))
|
||||
|
||||
assert len(b._default) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
b._parse_default()
|
||||
|
||||
assert len(b._default) == 1
|
||||
assert b.config == {'foo': 'bar'}
|
||||
|
||||
def test_parse_env_env_prefix(self, with_env):
|
||||
"""Parse the environment variable configuration."""
|
||||
b = bison.Bison()
|
||||
b.env_prefix = 'TEST_ENV_'
|
||||
b.auto_env = True
|
||||
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
b._parse_env()
|
||||
|
||||
# no scheme means nothing parsed.
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
def test_parse_env_env_prefix2(self, with_env):
|
||||
"""Parse the environment variable configuration."""
|
||||
b = bison.Bison()
|
||||
# here, do not include the trailing '_', this should be added
|
||||
# on automatically if not there.
|
||||
b.env_prefix = 'TEST_ENV'
|
||||
b.auto_env = True
|
||||
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
b._parse_env()
|
||||
|
||||
# no scheme means nothing parsed.
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
def test_parse_env_bind_env(self, with_env):
|
||||
"""Parse the environment variable configuration."""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', bind_env='FOO')
|
||||
))
|
||||
b.env_prefix = 'TEST_ENV'
|
||||
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
b._parse_env()
|
||||
|
||||
# we should get nothing back here -- env parsing will NOT
|
||||
# use the env_prefix when bind_env is specified manually.
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
def test_parse_env_bind_env_no_prefix(self, with_env):
|
||||
"""Parse the environment variable configuration."""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', bind_env='TEST_ENV_FOO')
|
||||
))
|
||||
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
b._parse_env()
|
||||
|
||||
assert len(b._environment) == 1
|
||||
assert b.config == {
|
||||
'foo': 'bar'
|
||||
}
|
||||
|
||||
def test_parse_env_bind_env_auto(self, with_env):
|
||||
"""Parse the environment variable configuration."""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
# this will autoenv to TEST_ENV_FOO, so we set to something
|
||||
# different here to test that it a.) works, b.) overrides autoenv
|
||||
bison.Option('foo', bind_env='TEST_OTHER_ENV_BAR'),
|
||||
bison.DictOption('nested', scheme=bison.Scheme(
|
||||
bison.DictOption('env', scheme=bison.Scheme(
|
||||
bison.Option('key')
|
||||
))
|
||||
))
|
||||
))
|
||||
b.env_prefix = 'TEST_ENV'
|
||||
b.auto_env = True
|
||||
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
b._parse_env()
|
||||
|
||||
assert len(b._environment) == 2
|
||||
assert b.config == {
|
||||
'foo': 'baz',
|
||||
'nested': {
|
||||
'env': {
|
||||
'key': 'test'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_parse_env_bind_env_false(self, with_env):
|
||||
"""Parse the environment variable configuration."""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', bind_env=False)
|
||||
))
|
||||
b.env_prefix = 'TEST_ENV'
|
||||
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
b._parse_env()
|
||||
|
||||
# we should get nothing back here -- nothing configured for
|
||||
# env parsing
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
def test_parse_env_bind_env_false_no_prefix(self, with_env):
|
||||
"""Parse the environment variable configuration."""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', bind_env=False)
|
||||
))
|
||||
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
b._parse_env()
|
||||
|
||||
# we should get nothing back here -- nothing configured for
|
||||
# env parsing
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
def test_parse_env_bind_env_false_auto(self, with_env):
|
||||
"""Parse the environment variable configuration."""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', bind_env=False)
|
||||
))
|
||||
b.env_prefix = 'TEST_ENV'
|
||||
b.auto_env = True
|
||||
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
b._parse_env()
|
||||
|
||||
# we should not get back the 'foo' key since its disabled,
|
||||
# even with auto-env
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
def test_parse_env_bind_env_true(self, with_env):
|
||||
"""Parse the environment variable configuration."""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', bind_env=True)
|
||||
))
|
||||
b.env_prefix = 'TEST_ENV'
|
||||
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
b._parse_env()
|
||||
|
||||
assert len(b._environment) == 1
|
||||
assert b.config == {
|
||||
'foo': 'bar'
|
||||
}
|
||||
|
||||
def test_parse_env_bind_env_true_no_prefix(self, with_env):
|
||||
"""Parse the environment variable configuration."""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', bind_env=True)
|
||||
))
|
||||
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
b._parse_env()
|
||||
|
||||
# we should not get anything back since we don't have a 'FOO' env variable
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
def test_parse_env_bind_env_true_auto(self, with_env):
|
||||
"""Parse the environment variable configuration."""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', bind_env=True),
|
||||
bison.DictOption('nested', scheme=bison.Scheme(
|
||||
bison.DictOption('env', scheme=bison.Scheme(
|
||||
bison.Option('key')
|
||||
))
|
||||
))
|
||||
))
|
||||
b.env_prefix = 'TEST_ENV'
|
||||
b.auto_env = True
|
||||
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
b._parse_env()
|
||||
|
||||
assert len(b._environment) == 2
|
||||
assert b.config == {
|
||||
'foo': 'bar',
|
||||
'nested': {
|
||||
'env': {
|
||||
'key': 'test'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_parse_env_int_value(self, with_env):
|
||||
"""Parse the environment variable configuration."""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', bind_env='FOO_INT', field_type=int)
|
||||
))
|
||||
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
b._parse_env()
|
||||
|
||||
assert len(b._environment) == 1
|
||||
assert b.config == {
|
||||
'foo': 1,
|
||||
}
|
||||
|
||||
def test_parse_env_bool_value(self, with_env):
|
||||
"""Parse the environment variable configuration."""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', bind_env='FOO_BOOL', field_type=bool)
|
||||
))
|
||||
|
||||
assert len(b._environment) == 0
|
||||
assert len(b.config) == 0
|
||||
|
||||
b._parse_env()
|
||||
|
||||
assert len(b._environment) == 1
|
||||
assert b.config == {
|
||||
'foo': False,
|
||||
}
|
|
@ -0,0 +1,569 @@
|
|||
"""Unit tests for bison.scheme"""
|
||||
|
||||
import pytest
|
||||
|
||||
from bison import errors, scheme
|
||||
|
||||
|
||||
def test_base_opt():
|
||||
"""Validate the base option, which should fail."""
|
||||
opt = scheme._BaseOpt()
|
||||
with pytest.raises(NotImplementedError):
|
||||
opt.validate('test-data')
|
||||
|
||||
|
||||
class TestOption:
|
||||
"""Tests for the `Option` class."""
|
||||
|
||||
def test_init_simple(self):
|
||||
"""Initialize an Option."""
|
||||
opt = scheme.Option('test-opt')
|
||||
|
||||
assert opt.name == 'test-opt'
|
||||
assert type(opt.default) == scheme.NoDefault
|
||||
assert opt.type is None
|
||||
assert opt.choices is None
|
||||
assert opt.bind_env is None
|
||||
|
||||
def test_init_full(self):
|
||||
"""Initialize an Option."""
|
||||
opt = scheme.Option(
|
||||
name='test-opt',
|
||||
default='foo',
|
||||
field_type=str,
|
||||
choices=['foo', 'bar'],
|
||||
bind_env=True
|
||||
)
|
||||
|
||||
assert opt.name == 'test-opt'
|
||||
assert type(opt.default) != scheme.NoDefault
|
||||
assert opt.default == 'foo'
|
||||
assert opt.type == str
|
||||
assert opt.choices == ['foo', 'bar']
|
||||
assert opt.bind_env is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'field_type,value', [
|
||||
(str, 'test-value'),
|
||||
(str, ''),
|
||||
(int, 0),
|
||||
(int, 1000),
|
||||
(int, -1),
|
||||
(float, 0.0),
|
||||
(float, 1000.999),
|
||||
(float, -1.0),
|
||||
(bool, True),
|
||||
(bool, False),
|
||||
(list, []),
|
||||
(list, [1, 2, 3]),
|
||||
(list, ['a', 'b', 'c']),
|
||||
(tuple, tuple()),
|
||||
(tuple, (1,)),
|
||||
(tuple, ('a', 'b')),
|
||||
(dict, {}),
|
||||
(dict, {'a': 'b'}),
|
||||
(dict, {1: 2})
|
||||
]
|
||||
)
|
||||
def test_validate_type_ok(self, field_type, value):
|
||||
"""Validate an Option, where type validation succeeds"""
|
||||
opt = scheme.Option('test-option', field_type=field_type)
|
||||
opt.validate(value)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'field_type,value', [
|
||||
(str, None),
|
||||
(str, 1),
|
||||
(str, 1.8),
|
||||
(str, True),
|
||||
(str, ['a', 'b', 'c']),
|
||||
|
||||
(int, None),
|
||||
(int, 'value'),
|
||||
(int, 1.8),
|
||||
(int, True),
|
||||
(int, ['a', 'b', 'c']),
|
||||
|
||||
(float, None),
|
||||
(float, 'value'),
|
||||
(float, 1),
|
||||
(float, True),
|
||||
(float, ['a', 'b', 'c']),
|
||||
|
||||
(bool, None),
|
||||
(bool, 'value'),
|
||||
(bool, 1),
|
||||
(bool, 1.8),
|
||||
(bool, ['a', 'b', 'c']),
|
||||
|
||||
(list, None),
|
||||
(list, 'value'),
|
||||
(list, 1),
|
||||
(list, 1.8),
|
||||
(list, True),
|
||||
]
|
||||
)
|
||||
def test_validate_type_failure(self, field_type, value):
|
||||
"""Validate an Option, where type validation fails"""
|
||||
opt = scheme.Option('test-option', field_type=field_type)
|
||||
with pytest.raises(errors.SchemeValidationError):
|
||||
opt.validate(value)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'choices,value', [
|
||||
(['one', 'two'], 'one'), # choices can be lists
|
||||
(('one', 'two'), 'one'), # choices can be tuples
|
||||
([1, 2, 3], 1),
|
||||
((1, 2, 3), 1),
|
||||
([None], None),
|
||||
((None,), None),
|
||||
([1.21, 1.22, 1.23], 1.23),
|
||||
((1.21, 1.22, 1.23), 1.23),
|
||||
([True, False], False),
|
||||
((True, False), False)
|
||||
]
|
||||
)
|
||||
def test_validate_choices_ok(self, choices, value):
|
||||
"""Validate an Option, where choice validation succeeds"""
|
||||
opt = scheme.Option('test-option', choices=choices)
|
||||
opt.validate(value)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'choices,value', [
|
||||
(['one', 'two'], 'three'),
|
||||
([1, 2, 3], 0),
|
||||
([], None),
|
||||
([0.2, 0.3, 0.4], 0.1),
|
||||
([False], True)
|
||||
]
|
||||
)
|
||||
def test_validate_choices_failure(self, choices, value):
|
||||
"""Validate an Option, where choice validation succeeds"""
|
||||
opt = scheme.Option('test-option', choices=choices)
|
||||
with pytest.raises(errors.SchemeValidationError):
|
||||
opt.validate(value)
|
||||
|
||||
|
||||
class TestDictOption:
|
||||
"""Tests for the `DictOption` class."""
|
||||
|
||||
def test_init_simple(self):
|
||||
"""Initialize a DictOption."""
|
||||
opt = scheme.DictOption('test-opt', None)
|
||||
|
||||
assert opt.name == 'test-opt'
|
||||
assert type(opt.default) == scheme.NoDefault
|
||||
assert opt.scheme is None
|
||||
assert opt.bind_env is False
|
||||
|
||||
def test_init_full(self):
|
||||
"""Initialize a DictOption."""
|
||||
opt = scheme.DictOption(
|
||||
name='test-opt',
|
||||
scheme=scheme.Scheme(),
|
||||
default='foo',
|
||||
bind_env=True
|
||||
)
|
||||
|
||||
assert opt.name == 'test-opt'
|
||||
assert type(opt.default) != scheme.NoDefault
|
||||
assert opt.default == 'foo'
|
||||
assert isinstance(opt.scheme, scheme.Scheme)
|
||||
assert opt.bind_env is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value', [
|
||||
'foo',
|
||||
1,
|
||||
1.234,
|
||||
False,
|
||||
True,
|
||||
None,
|
||||
[1, 2, 3],
|
||||
['a', 'b', 'c'],
|
||||
[{'a': 1}, {'b': 2}],
|
||||
('foo', 'bar'),
|
||||
{1, 2, 3}
|
||||
]
|
||||
)
|
||||
def test_validate_bad_data(self, value):
|
||||
"""Validate a DictOption where the given value is not a dict"""
|
||||
opt = scheme.DictOption('test-opt', scheme.Scheme())
|
||||
with pytest.raises(errors.SchemeValidationError):
|
||||
opt.validate(value)
|
||||
|
||||
def test_validate_no_scheme(self):
|
||||
"""Validate a DictOption with no scheme"""
|
||||
opt = scheme.DictOption('test-opt', None)
|
||||
opt.validate({'foo': 'bar'})
|
||||
|
||||
def test_validate_with_scheme(self):
|
||||
"""Validate a DictOption with a scheme"""
|
||||
opt = scheme.DictOption('test-opt', scheme.Scheme(
|
||||
scheme.Option('foo', field_type=str)
|
||||
))
|
||||
opt.validate({'foo': 'bar'})
|
||||
|
||||
|
||||
class TestListOption:
|
||||
"""Tests for the `ListOption` class."""
|
||||
|
||||
def test_init_simple(self):
|
||||
"""Initialize a ListOption."""
|
||||
opt = scheme.ListOption('test-opt')
|
||||
|
||||
assert opt.name == 'test-opt'
|
||||
assert type(opt.default) == scheme.NoDefault
|
||||
assert opt.member_type is None
|
||||
assert opt.member_scheme is None
|
||||
assert opt.bind_env is False
|
||||
|
||||
def test_init_full(self):
|
||||
"""Initialize a ListOption."""
|
||||
opt = scheme.ListOption(
|
||||
name='test-opt',
|
||||
default='foo',
|
||||
member_type=dict,
|
||||
member_scheme=scheme.Scheme(),
|
||||
bind_env=True
|
||||
)
|
||||
|
||||
assert opt.name == 'test-opt'
|
||||
assert type(opt.default) != scheme.NoDefault
|
||||
assert opt.default == 'foo'
|
||||
assert opt.member_type == dict
|
||||
assert isinstance(opt.member_scheme, scheme.Scheme)
|
||||
assert opt.bind_env is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value', [
|
||||
'foo',
|
||||
1,
|
||||
1.234,
|
||||
False,
|
||||
True,
|
||||
None,
|
||||
{'a': 1, 'b': 2},
|
||||
('foo', 'bar'),
|
||||
{1, 2, 3}
|
||||
]
|
||||
)
|
||||
def test_validate_bad_data(self, value):
|
||||
"""Validate when the value is not a list"""
|
||||
opt = scheme.ListOption('test-opt')
|
||||
with pytest.raises(errors.SchemeValidationError):
|
||||
opt.validate(value)
|
||||
|
||||
def test_validate_member_type_scheme_conflict(self):
|
||||
"""Validate the ListOption when both member_type and member_scheme are defined."""
|
||||
opt = scheme.ListOption(
|
||||
name='test-opt',
|
||||
member_type=int,
|
||||
member_scheme=scheme.Scheme()
|
||||
)
|
||||
with pytest.raises(errors.SchemeValidationError):
|
||||
opt.validate([1, 2, 3])
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'member_type,value', [
|
||||
(str, ['a', 'b', 'c']),
|
||||
(int, [1, 2, 3]),
|
||||
(float, [1.0, 2.0, 3.0]),
|
||||
(bool, [False, False, True]),
|
||||
(tuple, [(1,), (2,), (3,)]),
|
||||
(list, [[1], [2], [3]]),
|
||||
(dict, [{'a': 1, 'b': 2}])
|
||||
]
|
||||
)
|
||||
def test_validate_member_type_ok(self, member_type, value):
|
||||
"""Validate the ListOption, where member_type validation succeeds."""
|
||||
opt = scheme.ListOption('test-opt', member_type=member_type)
|
||||
opt.validate(value)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'member_type,value', [
|
||||
(str, ['a', 1]),
|
||||
(str, [1, 2]),
|
||||
(int, [1, 2, '3']),
|
||||
(int, ['foo', 'bar']),
|
||||
(float, [1.0, '2.0', 3.0]),
|
||||
(float, ['foo', 'bar']),
|
||||
(bool, ['False', False, True]),
|
||||
(bool, ['foo', 'bar']),
|
||||
(tuple, [(1,), '(2,)', (3,)]),
|
||||
(tuple, ['']),
|
||||
(list, [[1], (2,), [3]]),
|
||||
(list, ['foo', 'bar']),
|
||||
(dict, [{'a': 1}, {1, 2, 3}]),
|
||||
(dict, ['foo', 'bar'])
|
||||
]
|
||||
)
|
||||
def test_validate_member_type_failure(self, member_type, value):
|
||||
"""Validate the ListOption, where member_type validation fails."""
|
||||
opt = scheme.ListOption('test-opt', member_type=member_type)
|
||||
with pytest.raises(errors.SchemeValidationError):
|
||||
opt.validate(value)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'member_scheme,value', [
|
||||
# an empty scheme will validate every dict as correct
|
||||
(scheme.Scheme(), [{'foo': 'bar'}]),
|
||||
(scheme.Scheme(), [{1: 3}]),
|
||||
(scheme.Scheme(), [{1.23: 2.31}]),
|
||||
(scheme.Scheme(), [{False: True}]),
|
||||
(scheme.Scheme(), [{None: None}]),
|
||||
(scheme.Scheme(), [{(1, 2): (2, 1)}]),
|
||||
|
||||
(scheme.Scheme(scheme.Option('foo', field_type=str)), [{'foo': 'bar'}]),
|
||||
(scheme.Scheme(scheme.Option('foo', field_type=str)), [{'foo': 'baz'}]),
|
||||
(scheme.Scheme(scheme.Option('foo', field_type=str)), [{'foo': 'baz'}, {'foo': 'bar'}])
|
||||
]
|
||||
)
|
||||
def test_validate_member_scheme_ok(self, member_scheme, value):
|
||||
"""Validate the ListOption, where member_scheme validation succeeds."""
|
||||
opt = scheme.ListOption('test-opt', member_scheme=member_scheme)
|
||||
opt.validate(value)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'member_scheme,value', [
|
||||
(scheme.Scheme(scheme.Option('foo', field_type=str)), [{'foo': 1}]),
|
||||
(scheme.Scheme(scheme.Option('foo', field_type=str)), [{'foo': 1.23}]),
|
||||
(scheme.Scheme(scheme.Option('foo', field_type=str)), [{'foo': False}]),
|
||||
(scheme.Scheme(scheme.Option('foo', field_type=str)), [{'foo': True}]),
|
||||
(scheme.Scheme(scheme.Option('foo', field_type=str)), [{'foo': None}]),
|
||||
(scheme.Scheme(scheme.Option('foo', field_type=str)), [{'foo': (1, 2)}]),
|
||||
(scheme.Scheme(scheme.Option('foo', field_type=str)), [{'foo': ['a', 'b']}]),
|
||||
(scheme.Scheme(scheme.Option('foo', field_type=str)), [{'foo': {'a', 'b'}}]),
|
||||
|
||||
(scheme.Scheme(scheme.Option('bar', field_type=int)), [{'bar': 'foo'}]),
|
||||
(scheme.Scheme(scheme.Option('bar', field_type=int)), [{'bar': 1.23}]),
|
||||
(scheme.Scheme(scheme.Option('bar', field_type=int)), [{'bar': False}]),
|
||||
(scheme.Scheme(scheme.Option('bar', field_type=int)), [{'bar': True}]),
|
||||
(scheme.Scheme(scheme.Option('bar', field_type=int)), [{'bar': None}]),
|
||||
(scheme.Scheme(scheme.Option('bar', field_type=int)), [{'bar': (1, 2)}]),
|
||||
(scheme.Scheme(scheme.Option('bar', field_type=int)), [{'bar': ['a', 'b']}]),
|
||||
(scheme.Scheme(scheme.Option('bar', field_type=int)), [{'bar': {'a', 'b'}}])
|
||||
]
|
||||
)
|
||||
def test_validate_member_scheme_fail(self, member_scheme, value):
|
||||
"""Validate the ListOption, where member_scheme validation fails."""
|
||||
opt = scheme.ListOption('test-opt', member_scheme=member_scheme)
|
||||
with pytest.raises(errors.SchemeValidationError):
|
||||
opt.validate(value)
|
||||
|
||||
def test_validate_member_scheme_not_a_scheme(self):
|
||||
"""Validate the ListOption, where the member_scheme is not a Scheme."""
|
||||
opt = scheme.ListOption('test-opt', member_scheme='not-none-or-scheme')
|
||||
with pytest.raises(errors.SchemeValidationError):
|
||||
opt.validate(['a', 'b', 'c'])
|
||||
|
||||
|
||||
class TestScheme:
|
||||
"""Tests for the `Scheme` class."""
|
||||
|
||||
def test_empty_init(self):
|
||||
"""Initialize a Scheme with no arguments."""
|
||||
sch = scheme.Scheme()
|
||||
|
||||
assert len(sch.args) == 0
|
||||
assert sch._flat is None
|
||||
|
||||
def test_single_arg_init(self):
|
||||
"""Initialize a Scheme with one argument."""
|
||||
sch = scheme.Scheme(
|
||||
'item'
|
||||
)
|
||||
|
||||
assert len(sch.args) == 1
|
||||
assert sch._flat is None
|
||||
|
||||
def test_multi_arg_init(self):
|
||||
"""Initialize a Scheme with multiple arguments."""
|
||||
sch = scheme.Scheme(
|
||||
'item-1',
|
||||
'item-2',
|
||||
'item-3'
|
||||
)
|
||||
|
||||
assert len(sch.args) == 3
|
||||
assert sch._flat is None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'args,expected', [
|
||||
(
|
||||
# args
|
||||
(scheme.Option('foo', default='bar'),),
|
||||
# expected
|
||||
{'foo': 'bar'}
|
||||
),
|
||||
(
|
||||
# args
|
||||
(
|
||||
scheme.Option('foo'),
|
||||
scheme.Option('bar', default='baz'),
|
||||
scheme.ListOption('list', default=['a', 'b'])
|
||||
),
|
||||
# expected
|
||||
{
|
||||
'bar': 'baz',
|
||||
'list': ['a', 'b']
|
||||
}
|
||||
),
|
||||
(
|
||||
# args
|
||||
(
|
||||
scheme.DictOption('foo', scheme=scheme.Scheme(), default={}),
|
||||
scheme.DictOption('bar', scheme=scheme.Scheme(
|
||||
scheme.Option('test', default=True),
|
||||
scheme.Option('data', default=None),
|
||||
scheme.Option('value', default=20),
|
||||
scheme.Option('float', default=10.1010),
|
||||
scheme.Option('no_default'),
|
||||
scheme.DictOption('dct', scheme=scheme.Scheme(
|
||||
scheme.Option('nested', default='here')
|
||||
))
|
||||
))
|
||||
),
|
||||
# expected
|
||||
{
|
||||
'foo': {},
|
||||
'bar': {
|
||||
'test': True,
|
||||
'data': None,
|
||||
'value': 20,
|
||||
'float': 10.1010,
|
||||
'dct': {
|
||||
'nested': 'here'
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
def test_build_defaults(self, args, expected):
|
||||
"""Build a defaults dict from a Scheme."""
|
||||
sch = scheme.Scheme(*args)
|
||||
defaults = sch.build_defaults()
|
||||
|
||||
assert defaults == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'args', [
|
||||
('a', 'b'), # not an instance of _BaseOpt
|
||||
]
|
||||
)
|
||||
def test_build_defaults_failure(self, args):
|
||||
"""Build a defaults dict from a Scheme with bad data."""
|
||||
sch = scheme.Scheme(*args)
|
||||
with pytest.raises(errors.InvalidSchemeError):
|
||||
sch.build_defaults()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'args,expected', [
|
||||
(
|
||||
(scheme.Option('foo'),),
|
||||
['foo']
|
||||
),
|
||||
(
|
||||
(scheme.Option('foo'), scheme.Option('bar')),
|
||||
['foo', 'bar']
|
||||
),
|
||||
(
|
||||
(
|
||||
scheme.Option('foo'),
|
||||
scheme.DictOption('bar', scheme=scheme.Scheme(
|
||||
scheme.Option('test'),
|
||||
scheme.DictOption('dct', scheme=scheme.Scheme(
|
||||
scheme.Option('nested')
|
||||
)),
|
||||
scheme.ListOption('list')
|
||||
))
|
||||
),
|
||||
['foo', 'bar', 'bar.test', 'bar.dct', 'bar.list', 'bar.dct.nested']
|
||||
)
|
||||
]
|
||||
)
|
||||
def test_flatten(self, args, expected):
|
||||
"""Flatten a Scheme."""
|
||||
sch = scheme.Scheme(*args)
|
||||
flattened = sch.flatten()
|
||||
|
||||
assert len(flattened) == len(expected)
|
||||
for key in expected:
|
||||
assert key in flattened
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'args,value', [
|
||||
(
|
||||
# option exists in config
|
||||
(scheme.Option('foo', default='bar', field_type=str),),
|
||||
{'foo': 'baz'}
|
||||
),
|
||||
(
|
||||
# option does not exist in config, but has default
|
||||
(scheme.Option('foo', default='bar', field_type=str),),
|
||||
{}
|
||||
),
|
||||
(
|
||||
# multiple args
|
||||
(
|
||||
scheme.Option('foo', field_type=str),
|
||||
scheme.Option('bar', field_type=int),
|
||||
scheme.Option('baz', choices=['test'])
|
||||
),
|
||||
{'foo': 'a', 'bar': 1, 'baz': 'test'}
|
||||
)
|
||||
]
|
||||
)
|
||||
def test_validate_ok(self, args, value):
|
||||
"""Validate a Scheme successfully."""
|
||||
sch = scheme.Scheme(*args)
|
||||
sch.validate(value)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'args,value', [
|
||||
(
|
||||
# option does not exist in config, no default
|
||||
(scheme.Option('foo', field_type=str),),
|
||||
{}
|
||||
),
|
||||
(
|
||||
# option exists in config, fails validation
|
||||
(scheme.Option('foo', default='bar', field_type=str),),
|
||||
{'foo': 1}
|
||||
),
|
||||
(
|
||||
# multiple args, one fails validation
|
||||
(
|
||||
scheme.Option('foo', field_type=str),
|
||||
scheme.Option('bar', field_type=int),
|
||||
scheme.Option('baz', choices=['test'])
|
||||
),
|
||||
{'foo': 'a', 'bar': 1, 'baz': 'something'}
|
||||
)
|
||||
]
|
||||
)
|
||||
def test_validate_failure(self, args, value):
|
||||
"""Validate a Scheme unsuccessfully."""
|
||||
sch = scheme.Scheme(*args)
|
||||
with pytest.raises(errors.SchemeValidationError):
|
||||
sch.validate(value)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value', [
|
||||
'foo',
|
||||
1,
|
||||
1.23,
|
||||
['a', 'b', 'c'],
|
||||
{'a', 'b', 'c'},
|
||||
('a', 'b', 'c'),
|
||||
None,
|
||||
False,
|
||||
True
|
||||
]
|
||||
)
|
||||
def test_validate_failure_bad_config(self, value):
|
||||
"""Validate a Scheme where the given config is not a dict."""
|
||||
sch = scheme.Scheme()
|
||||
with pytest.raises(errors.SchemeValidationError):
|
||||
sch.validate(value)
|
|
@ -0,0 +1,270 @@
|
|||
"""Unit tests for bison.utils"""
|
||||
|
||||
import pytest
|
||||
|
||||
from bison import errors, scheme, utils
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'option,value,expected', [
|
||||
(scheme.Option('foo'), 'foo', 'foo'),
|
||||
(scheme.Option('foo'), 1, 1),
|
||||
(scheme.Option('foo'), None, None),
|
||||
(scheme.Option('foo'), False, False),
|
||||
(scheme.Option('foo', field_type=str), 'foo', 'foo'),
|
||||
(scheme.Option('foo', field_type=str), 1, '1'),
|
||||
(scheme.Option('foo', field_type=int), '1', 1),
|
||||
(scheme.Option('foo', field_type=float), '1', 1.0),
|
||||
(scheme.Option('foo', field_type=float), '1.23', 1.23),
|
||||
(scheme.Option('foo', field_type=bool), 'false', False),
|
||||
(scheme.Option('foo', field_type=bool), 'False', False),
|
||||
(scheme.Option('foo', field_type=bool), 'FALSE', False),
|
||||
(scheme.Option('foo', field_type=bool), 'true', True),
|
||||
(scheme.Option('foo', field_type=bool), 'True', True),
|
||||
(scheme.Option('foo', field_type=bool), 'TRUE', True),
|
||||
]
|
||||
)
|
||||
def test_cast(option, value, expected):
|
||||
"""Cast values to the type set by the Option."""
|
||||
actual = utils.cast(option, value)
|
||||
assert actual == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'option,value', [
|
||||
(scheme.Option('foo', field_type=int), 'foo'),
|
||||
(scheme.ListOption('foo'), 'foo'),
|
||||
(scheme.Option('foo', field_type=list), 'foo'),
|
||||
(scheme.Option('foo', field_type=tuple), 'foo'),
|
||||
]
|
||||
)
|
||||
def test_cast_fail(option, value):
|
||||
"""Cast values to the type set by the Option."""
|
||||
with pytest.raises(errors.BisonError):
|
||||
utils.cast(option, value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'key,value,expected', [
|
||||
('a', 'b', ('a', 'b')),
|
||||
('a.b', 'c', ('a', {'b': 'c'})),
|
||||
('a.b.c', None, ('a', {'b': {'c': None}})),
|
||||
('a.b.c', 1, ('a', {'b': {'c': 1}})),
|
||||
('a.b.c', True, ('a', {'b': {'c': True}}))
|
||||
]
|
||||
)
|
||||
def test_build_dot_value(key, value, expected):
|
||||
"""Test building new dictionaries based off of a dot notation key"""
|
||||
res = utils.build_dot_value(key, value)
|
||||
assert res == expected
|
||||
|
||||
|
||||
class TestDotDict:
|
||||
"""Tests for the DotDict class."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'param,expected', [
|
||||
(None, {}),
|
||||
({}, {}),
|
||||
({'a': 'b'}, {'a': 'b'}),
|
||||
({'a': 1, 'b': {'c': None}}, {'a': 1, 'b': {'c': None}})
|
||||
]
|
||||
)
|
||||
def test_init(self, param, expected):
|
||||
"""Initialize a new DotDict."""
|
||||
dd = utils.DotDict(param)
|
||||
assert dd == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'key,expected', [
|
||||
('z', None),
|
||||
('x.y.z', None),
|
||||
('c.d.g', None),
|
||||
('', None),
|
||||
('a', 1),
|
||||
('b', None),
|
||||
('c', {'f': 'bar', 'd': {'e': 'foo'}}),
|
||||
('c.d', {'e': 'foo'}),
|
||||
('c.d.e', 'foo'),
|
||||
('c.f', 'bar'),
|
||||
('g', False)
|
||||
]
|
||||
)
|
||||
def test_get(self, key, expected):
|
||||
"""Get values from the DotDict."""
|
||||
dd = utils.DotDict({
|
||||
'a': 1,
|
||||
'b': None,
|
||||
'c': {
|
||||
'd': {
|
||||
'e': 'foo'
|
||||
},
|
||||
'f': 'bar'
|
||||
},
|
||||
'g': False
|
||||
})
|
||||
|
||||
# test getting values via the .get() method
|
||||
value = dd.get(key)
|
||||
assert value == expected
|
||||
|
||||
# test getting values via dictionary access methods
|
||||
value = dd[key]
|
||||
assert value == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'key', [
|
||||
'a',
|
||||
'c',
|
||||
'c.d',
|
||||
'c.d.e',
|
||||
'c.f'
|
||||
]
|
||||
)
|
||||
def test_delete_1(self, key):
|
||||
"""Delete values from the DotDict that exist."""
|
||||
dd = utils.DotDict({
|
||||
'a': 1,
|
||||
'c': {
|
||||
'd': {
|
||||
'e': 'foo'
|
||||
},
|
||||
'f': 'bar'
|
||||
}
|
||||
})
|
||||
|
||||
value = dd.get(key)
|
||||
assert value is not None
|
||||
|
||||
# delete via the .delete() method
|
||||
dd.delete(key)
|
||||
|
||||
value = dd.get(key)
|
||||
assert value is None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'key', [
|
||||
'a',
|
||||
'c'
|
||||
]
|
||||
)
|
||||
def test_delete_2(self, key):
|
||||
"""Delete values from the DotDict using del"""
|
||||
dd = utils.DotDict({
|
||||
'a': 1,
|
||||
'c': {
|
||||
'd': {
|
||||
'e': 'foo'
|
||||
},
|
||||
'f': 'bar'
|
||||
}
|
||||
})
|
||||
|
||||
value = dd[key]
|
||||
assert value is not None
|
||||
|
||||
# delete via the del keyword. since we are testing
|
||||
# first order keys, they should all resolve, so they
|
||||
# can be deleted successfully.
|
||||
del dd[key]
|
||||
|
||||
value = dd[key]
|
||||
assert value is None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'key', [
|
||||
'x',
|
||||
'x.y.z',
|
||||
'',
|
||||
'c.e',
|
||||
'c.d.e',
|
||||
'c.d.e.f'
|
||||
]
|
||||
)
|
||||
def test_delete_3(self, key):
|
||||
"""Delete values that do not exist from the DotDict."""
|
||||
dd = utils.DotDict({
|
||||
'a': 1,
|
||||
'b': None,
|
||||
'c': {
|
||||
'd': 'foo',
|
||||
'f': 'bar'
|
||||
}
|
||||
})
|
||||
|
||||
# delete via the .delete() method
|
||||
with pytest.raises(KeyError):
|
||||
dd.delete(key)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'key', [
|
||||
'c.d',
|
||||
'c.d.e',
|
||||
'c.f'
|
||||
]
|
||||
)
|
||||
def test_delete_4(self, key):
|
||||
"""Delete values from the DotDict using del"""
|
||||
dd = utils.DotDict({
|
||||
'a': 1,
|
||||
'c': {
|
||||
'd': {
|
||||
'e': 'foo'
|
||||
},
|
||||
'f': 'bar'
|
||||
}
|
||||
})
|
||||
|
||||
# delete via the del keyword. dot notation keys
|
||||
# should fail resolution and raise a KeyError
|
||||
with pytest.raises(KeyError):
|
||||
del dd[key]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'key,value', [
|
||||
('a', 'foo'),
|
||||
('b', 'bar'),
|
||||
('b.c', False),
|
||||
('x', 1),
|
||||
('x.y', [1, 2, 3]),
|
||||
('x.y.z', 'baz'),
|
||||
('x.y.z.q', {'a': 'foo', 'b': [1, 2, 3]})
|
||||
]
|
||||
)
|
||||
def test_set(self, key, value):
|
||||
"""Set an item in the DotDict"""
|
||||
dd = utils.DotDict({
|
||||
'a': 1,
|
||||
'b': {'c': True}
|
||||
})
|
||||
|
||||
dd[key] = value
|
||||
assert dd.get(key) == value
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'key,expected', [
|
||||
('a', True),
|
||||
('a.b', False),
|
||||
('a.b.c', False),
|
||||
('b', True),
|
||||
('b.c', True),
|
||||
('b.c', True),
|
||||
('b.a', False),
|
||||
('b.c.d', True),
|
||||
('b.c.d.e', False),
|
||||
('b.c.d.e.f', False),
|
||||
('g', False),
|
||||
('g.e', False),
|
||||
]
|
||||
)
|
||||
def test_inclusion(self, key, expected):
|
||||
"""Check if a key exists in a DotDict"""
|
||||
dd = utils.DotDict({
|
||||
'a': 1,
|
||||
'b': {
|
||||
'c': {
|
||||
'd': 'foo'
|
||||
}
|
||||
}
|
||||
})
|
||||
assert (key in dd) is expected
|
Loading…
Reference in New Issue