Merge pull request #8 from edaniszewski/required-flag
remove implict 'required' from 'default' arg, add explicit 'required' arg
This commit is contained in:
commit
46b79c9ff1
2
Makefile
2
Makefile
|
@ -17,7 +17,7 @@ init: requires-pipenv ## Initialize the project for development
|
|||
|
||||
.PHONY: test
|
||||
test: ## Run the bison unit tests
|
||||
pipenv run py.test
|
||||
pipenv run py.test -vv
|
||||
|
||||
.PHONY: ci
|
||||
ci: coverage lint ## Run the ci pipeline (test w/ coverage, lint)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
__title__ = 'bison'
|
||||
__description__ = 'Python application configuration'
|
||||
__url__ = 'https://github.com/edaniszewski/bison'
|
||||
__version__ = '0.0.5'
|
||||
__version__ = '0.0.6'
|
||||
__author__ = 'Erick Daniszewski'
|
||||
__author_email__ = 'edaniszewski@gmail.com'
|
||||
__license__ = 'MIT'
|
||||
|
|
|
@ -54,7 +54,7 @@ class Scheme(object):
|
|||
defaults = {}
|
||||
for arg in self.args:
|
||||
if not isinstance(arg, _BaseOpt):
|
||||
raise errors.InvalidSchemeError('')
|
||||
raise errors.InvalidSchemeError('Unable to build default for non-Option type')
|
||||
|
||||
# if there is a default set, add it to the defaults dict
|
||||
if not isinstance(arg.default, NoDefault):
|
||||
|
@ -96,7 +96,7 @@ class Scheme(object):
|
|||
return self._flat
|
||||
|
||||
def validate(self, config):
|
||||
"""Validate the given config against the `Schema`.
|
||||
"""Validate the given config against the `Scheme`.
|
||||
|
||||
Args:
|
||||
config (dict): The configuration to validate.
|
||||
|
@ -118,13 +118,11 @@ class Scheme(object):
|
|||
|
||||
# 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:
|
||||
# if the option is not required, then it is fine to omit.
|
||||
# otherwise, its omission constitutes a validation error.
|
||||
if arg.required:
|
||||
raise errors.SchemeValidationError(
|
||||
'Option "{}" is expected but not found.'.format(arg.name)
|
||||
'Option "{}" is required, but not found.'.format(arg.name)
|
||||
)
|
||||
|
||||
|
||||
|
@ -180,20 +178,23 @@ class Option(_BaseOpt):
|
|||
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.
|
||||
required (bool): A flag that determines whether the option is required
|
||||
or not. If required and the option is not present in the config,
|
||||
validation will fail. If optional and not present in the config,
|
||||
validation will not fail. By default, an option is required.
|
||||
default: The default value to use for the option. If the option is not
|
||||
found in the config, the default will be used (regardless of whether
|
||||
it is required or optional). By default, this is the internal
|
||||
`_NoDefault` type (this allows setting `None` as a default).
|
||||
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):
|
||||
def __init__(self, name, required=True, default=_no_default, field_type=None, choices=None, bind_env=None):
|
||||
super(Option, self).__init__()
|
||||
self.name = name
|
||||
self.required = required
|
||||
self.default = default
|
||||
self.type = field_type
|
||||
self.choices = choices
|
||||
|
@ -310,12 +311,14 @@ class DictOption(_BaseOpt):
|
|||
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.
|
||||
required (bool): A flag that determines whether the option is required
|
||||
or not. If required and the option is not present in the config,
|
||||
validation will fail. If optional and not present in the config,
|
||||
validation will not fail. By default, an option is required.
|
||||
default: The default value to use for the option. If the option is not
|
||||
found in the config, the default will be used (regardless of whether
|
||||
it is required or optional). By default, this is the internal
|
||||
`_NoDefault` type (this allows setting `None` as a default).
|
||||
bind_env (bool): Bind the option to an environment variable. If False,
|
||||
the option will not be bound to env. If True, the key for the this
|
||||
DictOption will serve as an environment prefix. Any environment
|
||||
|
@ -323,9 +326,10 @@ class DictOption(_BaseOpt):
|
|||
as a string.
|
||||
"""
|
||||
|
||||
def __init__(self, name, scheme, default=_no_default, bind_env=False):
|
||||
def __init__(self, name, scheme, required=True, default=_no_default, bind_env=False):
|
||||
super(DictOption, self).__init__()
|
||||
self.name = name
|
||||
self.required = required
|
||||
self.default = default
|
||||
self.scheme = scheme
|
||||
self.bind_env = bind_env
|
||||
|
@ -382,12 +386,14 @@ class ListOption(_BaseOpt):
|
|||
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.
|
||||
required (bool): A flag that determines whether the option is required
|
||||
or not. If required and the option is not present in the config,
|
||||
validation will fail. If optional and not present in the config,
|
||||
validation will not fail. By default, an option is required.
|
||||
default: The default value to use for the option. If the option is not
|
||||
found in the config, the default will be used (regardless of whether
|
||||
it is required or optional). By default, this is the internal
|
||||
`_NoDefault` type (this allows setting `None` as a default).
|
||||
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.
|
||||
|
@ -398,9 +404,10 @@ class ListOption(_BaseOpt):
|
|||
items being comma separated.
|
||||
"""
|
||||
|
||||
def __init__(self, name, default=_no_default, member_type=None, member_scheme=None, bind_env=False):
|
||||
def __init__(self, name, required=True, default=_no_default, member_type=None, member_scheme=None, bind_env=False):
|
||||
super(ListOption, self).__init__()
|
||||
self.name = name
|
||||
self.required = required
|
||||
self.default = default
|
||||
self.member_type = member_type
|
||||
self.member_scheme = member_scheme
|
||||
|
|
|
@ -21,6 +21,22 @@ def yaml_config(tmpdir):
|
|||
cfg.remove()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def yaml_optional_nested(tmpdir):
|
||||
"""Create a YAML config file with optional nested values."""
|
||||
cfg = tmpdir.join('config.yml')
|
||||
cfg.write("""
|
||||
foo: True
|
||||
nested1:
|
||||
x: abc
|
||||
y: def
|
||||
""")
|
||||
|
||||
yield cfg
|
||||
|
||||
cfg.remove()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def bad_yaml_config(tmpdir):
|
||||
"""Create a bad YAML config file."""
|
||||
|
|
|
@ -270,12 +270,6 @@ class TestBison:
|
|||
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()
|
||||
|
@ -301,6 +295,12 @@ class TestBison:
|
|||
with pytest.raises(errors.BisonError):
|
||||
b._find_config()
|
||||
|
||||
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_parse_config_no_paths(self):
|
||||
"""Parse the file config when no paths are specified"""
|
||||
b = bison.Bison()
|
||||
|
@ -643,3 +643,206 @@ class TestBison:
|
|||
assert b.config == {
|
||||
'foo': False,
|
||||
}
|
||||
|
||||
def test_parse_validate_nested_optional(self, yaml_optional_nested):
|
||||
"""Parse a config for a scheme with a non-required option with
|
||||
required nested options where the non-required option is not set.
|
||||
"""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', bind_env=True),
|
||||
bison.DictOption('nested2', required=False, bind_env=True, scheme=bison.Scheme(
|
||||
bison.Option('x', field_type=str),
|
||||
bison.Option('y', field_type=str)
|
||||
))
|
||||
))
|
||||
b.add_config_paths(yaml_optional_nested.dirname)
|
||||
|
||||
assert b.config_file is None
|
||||
assert len(b._config) == 0
|
||||
|
||||
b.parse(requires_cfg=False)
|
||||
|
||||
assert b.config_file == os.path.join(yaml_optional_nested.dirname, yaml_optional_nested.basename)
|
||||
assert len(b._config) == 2
|
||||
assert b.config == {
|
||||
'foo': True,
|
||||
'nested1': {
|
||||
'x': 'abc',
|
||||
'y': 'def'
|
||||
}
|
||||
}
|
||||
|
||||
b.validate()
|
||||
|
||||
def test_parse_validate_nested_optional2(self, yaml_optional_nested):
|
||||
"""Parse a config for a scheme with a non-required option with
|
||||
required nested options where the non-required option is set.
|
||||
"""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', bind_env=True),
|
||||
bison.DictOption('nested1', required=False, bind_env=True, scheme=bison.Scheme(
|
||||
bison.Option('x', field_type=str),
|
||||
bison.Option('y', field_type=str)
|
||||
))
|
||||
))
|
||||
b.add_config_paths(yaml_optional_nested.dirname)
|
||||
|
||||
assert b.config_file is None
|
||||
assert len(b._config) == 0
|
||||
|
||||
b.parse(requires_cfg=False)
|
||||
|
||||
assert b.config_file == os.path.join(yaml_optional_nested.dirname, yaml_optional_nested.basename)
|
||||
assert len(b._config) == 2
|
||||
assert b.config == {
|
||||
'foo': True,
|
||||
'nested1': {
|
||||
'x': 'abc',
|
||||
'y': 'def'
|
||||
}
|
||||
}
|
||||
|
||||
b.validate()
|
||||
|
||||
def test_parse_validate_nested_optional3(self, yaml_optional_nested):
|
||||
"""Parse a config for a scheme with a non-required option with
|
||||
required nested options where the non-required option is not set,
|
||||
and the required options have defaults.
|
||||
"""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', bind_env=True),
|
||||
bison.DictOption('nested2', required=False, bind_env=True, scheme=bison.Scheme(
|
||||
bison.Option('x', default="abc", field_type=str),
|
||||
bison.Option('y', default="def", field_type=str)
|
||||
))
|
||||
))
|
||||
b.add_config_paths(yaml_optional_nested.dirname)
|
||||
|
||||
assert b.config_file is None
|
||||
assert len(b._config) == 0
|
||||
|
||||
b.parse(requires_cfg=False)
|
||||
|
||||
assert b.config_file == os.path.join(yaml_optional_nested.dirname, yaml_optional_nested.basename)
|
||||
assert len(b._config) == 2
|
||||
assert b.config == {
|
||||
'foo': True,
|
||||
'nested1': {
|
||||
'x': 'abc',
|
||||
'y': 'def'
|
||||
},
|
||||
'nested2': { # defaults get picked up
|
||||
'x': 'abc',
|
||||
'y': 'def'
|
||||
}
|
||||
}
|
||||
|
||||
b.validate()
|
||||
|
||||
def test_parse_validate_nested_optional4(self, yaml_optional_nested):
|
||||
"""Parse a config for a scheme with a non-required option with
|
||||
required nested options where the non-required option is not set
|
||||
and it has a default value.
|
||||
"""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', bind_env=True),
|
||||
bison.DictOption('nested2', required=False, default={}, bind_env=True, scheme=bison.Scheme(
|
||||
bison.Option('x', field_type=str),
|
||||
bison.Option('y', field_type=str)
|
||||
))
|
||||
))
|
||||
b.add_config_paths(yaml_optional_nested.dirname)
|
||||
|
||||
assert b.config_file is None
|
||||
assert len(b._config) == 0
|
||||
|
||||
b.parse(requires_cfg=False)
|
||||
|
||||
assert b.config_file == os.path.join(yaml_optional_nested.dirname, yaml_optional_nested.basename)
|
||||
assert len(b._config) == 2
|
||||
assert b.config == {
|
||||
'foo': True,
|
||||
'nested1': {
|
||||
'x': 'abc',
|
||||
'y': 'def'
|
||||
},
|
||||
'nested2': {} # default gets picked up
|
||||
}
|
||||
|
||||
# validation should fail -- the default value gets added, but the required
|
||||
# options are not set in the default
|
||||
with pytest.raises(errors.SchemeValidationError):
|
||||
b.validate()
|
||||
|
||||
def test_parse_validate_nested_optional5(self, yaml_optional_nested):
|
||||
"""Parse a config for a scheme with a non-required option with
|
||||
required nested options where the non-required option is not set
|
||||
and it has a default value.
|
||||
"""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', bind_env=True),
|
||||
bison.DictOption('nested2', required=False, default={'x': 'ghi', 'y': 'jkl'}, bind_env=True, scheme=bison.Scheme(
|
||||
bison.Option('x', field_type=str),
|
||||
bison.Option('y', field_type=str)
|
||||
))
|
||||
))
|
||||
b.add_config_paths(yaml_optional_nested.dirname)
|
||||
|
||||
assert b.config_file is None
|
||||
assert len(b._config) == 0
|
||||
|
||||
b.parse(requires_cfg=False)
|
||||
|
||||
assert b.config_file == os.path.join(yaml_optional_nested.dirname, yaml_optional_nested.basename)
|
||||
assert len(b._config) == 2
|
||||
assert b.config == {
|
||||
'foo': True,
|
||||
'nested1': {
|
||||
'x': 'abc',
|
||||
'y': 'def'
|
||||
},
|
||||
'nested2': { # default gets picked up
|
||||
'x': 'ghi',
|
||||
'y': 'jkl'
|
||||
}
|
||||
}
|
||||
|
||||
b.validate()
|
||||
|
||||
def test_parse_validate_nested_optional6(self, yaml_optional_nested):
|
||||
"""Parse a config for a scheme with a non-required option with
|
||||
required nested options where the non-required option is not set
|
||||
and it has a default value.
|
||||
|
||||
This test is the same as the one above, but the defaults are
|
||||
specified differently.
|
||||
"""
|
||||
b = bison.Bison(scheme=bison.Scheme(
|
||||
bison.Option('foo', bind_env=True),
|
||||
bison.DictOption('nested2', required=False, default={}, bind_env=True, scheme=bison.Scheme(
|
||||
bison.Option('x', default='ghi', field_type=str),
|
||||
bison.Option('y', default='jkl', field_type=str)
|
||||
))
|
||||
))
|
||||
b.add_config_paths(yaml_optional_nested.dirname)
|
||||
|
||||
assert b.config_file is None
|
||||
assert len(b._config) == 0
|
||||
|
||||
b.parse(requires_cfg=False)
|
||||
|
||||
assert b.config_file == os.path.join(yaml_optional_nested.dirname, yaml_optional_nested.basename)
|
||||
assert len(b._config) == 2
|
||||
assert b.config == {
|
||||
'foo': True,
|
||||
'nested1': {
|
||||
'x': 'abc',
|
||||
'y': 'def'
|
||||
},
|
||||
'nested2': { # default gets picked up
|
||||
'x': 'ghi',
|
||||
'y': 'jkl'
|
||||
}
|
||||
}
|
||||
|
||||
b.validate()
|
||||
|
|
|
@ -671,8 +671,8 @@ class TestScheme:
|
|||
{'foo': 'baz'}
|
||||
),
|
||||
(
|
||||
# option does not exist in config, but has default
|
||||
(scheme.Option('foo', default='bar', field_type=str),),
|
||||
# option does not exist in config and has default, but is not required
|
||||
(scheme.Option('foo', default='bar', required=False, field_type=str),),
|
||||
{}
|
||||
),
|
||||
(
|
||||
|
@ -683,6 +683,17 @@ class TestScheme:
|
|||
scheme.Option('baz', choices=['test'])
|
||||
),
|
||||
{'foo': 'a', 'bar': 1, 'baz': 'test'}
|
||||
),
|
||||
(
|
||||
# optional parent option not specified, required child option
|
||||
# not specified
|
||||
(
|
||||
scheme.DictOption('foo', required=False, scheme=scheme.Scheme(
|
||||
scheme.Option('bar', field_type=str),
|
||||
scheme.Option('baz', field_type=str),
|
||||
)),
|
||||
),
|
||||
{}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
@ -711,6 +722,17 @@ class TestScheme:
|
|||
scheme.Option('baz', choices=['test'])
|
||||
),
|
||||
{'foo': 'a', 'bar': 1, 'baz': 'something'}
|
||||
),
|
||||
(
|
||||
# optional parent option specified, required child option
|
||||
# not specified
|
||||
(
|
||||
scheme.DictOption('foo', required=True, scheme=scheme.Scheme(
|
||||
scheme.Option('bar', field_type=str),
|
||||
scheme.Option('baz', field_type=str),
|
||||
)),
|
||||
),
|
||||
{'foo': {'baz': 2}}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue