Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support "allow_none=True" in docval for args with non-None default #757

Merged
merged 2 commits into from
Aug 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/hdmf/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ def __parse_args(validator, args, kwargs, enforce_type=True, enforce_shape=True,
ret[argname] = _copy.deepcopy(arg['default'])
argval = ret[argname]
if enforce_type:
if not __type_okay(argval, arg['type'], arg['default'] is None):
if not __type_okay(argval, arg['type'], arg['default'] is None or arg.get('allow_none', False)):
if argval is None and arg['default'] is None:
fmt_val = (argname, __format_type(arg['type']))
type_errors.append("None is not allowed for '%s' (expected '%s', not None)" % fmt_val)
Expand Down Expand Up @@ -522,7 +522,9 @@ def docval(*validator, **options): # noqa: C901
must contain the following keys: ``'name'``, ``'type'``, and ``'doc'``. This will define a
positional argument. To define a keyword argument, specify a default value
using the key ``'default'``. To validate the dimensions of an input array
add the optional ``'shape'`` parameter.
add the optional ``'shape'`` parameter. To allow a None value for an argument,
either the default value must be None or a different default value must be provided
and ``'allow_none': True`` must be passed.

The decorated method must take ``self`` and ``**kwargs`` as arguments.

Expand Down Expand Up @@ -570,7 +572,7 @@ def dec(func):
kw = list()
for a in validator:
# catch unsupported keys
allowable_terms = ('name', 'doc', 'type', 'shape', 'enum', 'default', 'help')
allowable_terms = ('name', 'doc', 'type', 'shape', 'enum', 'default', 'allow_none', 'help')
unsupported_terms = set(a.keys()) - set(allowable_terms)
if unsupported_terms:
raise Exception('docval for {}: keys {} are not supported by docval'.format(a['name'],
Expand All @@ -596,6 +598,10 @@ def dec(func):
msg = ('docval for {}: enum values are of types not allowed by arg type (got {}, '
'expected {})'.format(a['name'], [type(x) for x in a['enum']], a['type']))
raise Exception(msg)
if a.get('allow_none', False) and 'default' not in a:
msg = ('docval for {}: allow_none=True can only be set if a default value is provided.').format(
a['name'])
raise Exception(msg)
if 'default' in a:
kw.append(a)
else:
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/utils_test/test_docval.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,61 @@ def method(self, **kwargs):
with self.assertRaisesWith(SyntaxError, msg):
method(self, True)

def test_allow_none_false(self):
"""Test that docval with allow_none=True and non-None default value works"""
@docval({'name': 'arg1', 'type': bool, 'doc': 'this is a bool or None with a default', 'default': True,
'allow_none': False})
def method(self, **kwargs):
return popargs('arg1', kwargs)

# if provided, None is not allowed
msg = ("TestDocValidator.test_allow_none_false.<locals>.method: incorrect type for 'arg1' "
"(got 'NoneType', expected 'bool')")
with self.assertRaisesWith(TypeError, msg):
res = method(self, arg1=None)

# if not provided, the default value is used
res = method(self)
self.assertTrue(res)

def test_allow_none(self):
"""Test that docval with allow_none=True and non-None default value works"""
@docval({'name': 'arg1', 'type': bool, 'doc': 'this is a bool or None with a default', 'default': True,
'allow_none': True})
def method(self, **kwargs):
return popargs('arg1', kwargs)

# if provided, None is allowed
res = method(self, arg1=None)
self.assertIsNone(res)

# if not provided, the default value is used
res = method(self)
self.assertTrue(res)

def test_allow_none_redundant(self):
"""Test that docval with allow_none=True and default=None works"""
@docval({'name': 'arg1', 'type': bool, 'doc': 'this is a bool or None with a default', 'default': None,
'allow_none': True})
def method(self, **kwargs):
return popargs('arg1', kwargs)

# if provided, None is allowed
res = method(self, arg1=None)
self.assertIsNone(res)

# if not provided, the default value is used
res = method(self)
self.assertIsNone(res)

def test_allow_none_no_default(self):
"""Test that docval with allow_none=True and no default raises an error"""
msg = ("docval for arg1: allow_none=True can only be set if a default value is provided.")
with self.assertRaisesWith(Exception, msg):
@docval({'name': 'arg1', 'type': bool, 'doc': 'this is a bool or None with a default', 'allow_none': True})
def method(self, **kwargs):
return popargs('arg1', kwargs)

def test_enum_str(self):
"""Test that the basic usage of an enum check on strings works"""
@docval({'name': 'arg1', 'type': str, 'doc': 'an arg', 'enum': ['a', 'b']}) # also use enum: list
Expand Down