Merge pull request #8 from edaniszewski/required-flag

remove implict 'required' from 'default' arg, add explicit 'required' arg
This commit is contained in:
Erick Daniszewski 2018-09-01 16:02:38 -04:00 committed by GitHub
commit 46b79c9ff1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 287 additions and 39 deletions

View File

@ -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)

View File

@ -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'

View File

@ -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

View File

@ -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."""

View 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()

View File

@ -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}}
)
]
)