Skip to content

Instantly share code, notes, and snippets.

@dimo414
Created March 5, 2020 05:00
Show Gist options
  • Save dimo414/b36b1622f751b578df06512957159f07 to your computer and use it in GitHub Desktop.
Save dimo414/b36b1622f751b578df06512957159f07 to your computer and use it in GitHub Desktop.
Detecting whether an argparse argument was set explicitly or not
class DefaultCapturingNamespace(argparse.Namespace):
"""An argparse Namespace that tracks the number of times the given argument
has been set. In theory this can allow you to distinguish between values
set by argparse (e.g. default values) and those specified by the user.
"""
def __init__(self, **kwargs):
type_name = type(self).__name__
argparse.Namespace.__setattr__(self, '_%s__original_values' % type_name, {})
argparse.Namespace.__setattr__(self, '_%s__set_counts' % type_name, {})
argparse.Namespace.__init__(self, **kwargs)
def _get_kwargs(self):
type_name = type(self).__name__
return sorted((k, v) for k, v in self.__dict__.items() if type_name not in k)
def __setattr__(self, name, value):
print("Called setattr(): %r: %r" % (name, value))
if type(self).__name__ in name:
raise ValueError('Illegal argument name: %s' % name)
if name not in self.__original_values:
self.__original_values[name] = value
self.__set_counts[name] = 1
else:
self.__set_counts[name] += 1
return argparse.Namespace.__setattr__(self, name, value)
def argument_set_count(self, name):
return self.__set_counts.get(name, 0)
def argument_original_value(self, name):
return self.__original_values[name]
"""
I accidentally stumbled down a rabbithole trying to do something I thought was
trivial - determine whether an argparse argument was specified explicitly on
the command line or not - even if the given value was the default.
There's some discussion in questions like
http://stackoverflow.com/q/30487767/113632 and
http://stackoverflow.com/q/32056910/113632
but no great solutions.
I had the idea to use a custom Namespace to determine how many times a value
was set (i.e. set more than once means the default was set, then the
user-specified value overwrote it). Unfortunately argparse doesn't consistently
follow this pattern - depending on the argument's settings it could be set more
or less times. It also seemed rather bittle to depend on the exact number of
times a value is set.
I next tried monkey-patching argparse to flip a flag in the namespace when it
starts parsing the user's inputs. This seems cleaner but is still imperfect;
default values are still reported as having been "set".
See the unit tests for example behavior.
argparse source: https://hg.python.org/cpython/file/3.6/Lib/argparse.py
"""
class ExplicitlySetNamespace(argparse.Namespace):
"""An argparse Namespace that, with help from a monkey-patch, can
distinguish between values set by default vs. values parsed from the
command line.
"""
def __init__(self, **kwargs):
type_name = type(self).__name__
argparse.Namespace.__setattr__(self, '_%s__is_explicit' % type_name, False)
argparse.Namespace.__setattr__(self, '_%s__default_values' % type_name, {})
argparse.Namespace.__setattr__(self, '_%s__explicit_values' % type_name, {})
argparse.Namespace.__init__(self, **kwargs)
def _get_kwargs(self):
type_name = type(self).__name__
return sorted((k, v) for k, v in self.__dict__.items() if type_name not in k)
def __setattr__(self, name, value):
print("Called setattr(): %r: %r" % (name, value))
if type(self).__name__ in name:
raise ValueError('Illegal argument name: %s' % name)
if not self.__is_explicit:
self.__default_values[name] = value
else:
self.__explicit_values[name] = value
return argparse.Namespace.__setattr__(self, name, value)
def apply_monkey_patch(self, parser):
def decorate(f):
def patch(*args, **kwargs):
argparse.Namespace.__setattr__(
self, '_%s__is_explicit' % type(self).__name__, True)
return f(*args, **kwargs)
return patch
parser._parse_known_args = decorate(parser._parse_known_args)
def argument_set_explicitly(self, name):
return name in self.__explicit_values
def argument_original_value(self, name):
return self.__default_values[name]
import default_detect
def test_DefaultCapturingNamespace():
namespace = default_detect.DefaultCapturingNamespace()
namespace.foo = 'bar'
# doesn't display private fields
assert str(namespace) == "DefaultCapturingNamespace(foo='bar')"
assert namespace.foo == 'bar'
assert namespace.argument_set_count('foo') == 1
assert namespace.argument_original_value('foo') == 'bar'
namespace.foo = 'baz'
assert str(namespace) == "DefaultCapturingNamespace(foo='baz')"
assert namespace.foo == 'baz'
assert namespace.argument_set_count('foo') == 2
assert namespace.argument_original_value('foo') == 'bar'
def test_DefaultCapturingNamespace_withParser():
parser = argparse.ArgumentParser()
parser.add_argument('foo', nargs='*')
parser.add_argument('--bar')
parser.add_argument('--baz', action='store_true')
parser.add_argument('--bang', default='biff')
parser.add_argument('--boom', default=argparse.SUPPRESS)
args = parser.parse_args(args=[], namespace=default_detect.DefaultCapturingNamespace())
assert args.argument_set_count('foo') == 2
assert args.argument_set_count('bar') == 1
assert args.argument_set_count('baz') == 1
assert args.argument_set_count('bang') == 2
assert args.argument_set_count('boom') == 0
print()
a=['foo', '--bar', 'bar', '--baz', '--bang', 'bang', '--boom', 'boom']
args = parser.parse_args(args=a, namespace=default_detect.DefaultCapturingNamespace())
assert args.argument_set_count('foo') == 2
assert args.argument_original_value('foo') is None
assert args.argument_set_count('bar') == 2
assert args.argument_original_value('bar') is None
assert args.argument_set_count('baz') == 2
assert args.argument_original_value('baz') == False
assert args.argument_set_count('bang') == 2
assert args.argument_original_value('bang') == 'biff'
assert args.argument_set_count('boom') == 1
assert args.argument_original_value('boom') is 'boom'
def test_ExplicitlySetNamespace():
namespace = default_detect.ExplicitlySetNamespace()
namespace.foo = 'bar'
# doesn't display private fields
assert str(namespace) == "ExplicitlySetNamespace(foo='bar')"
assert namespace.foo == 'bar'
assert not namespace.argument_set_explicitly('foo')
assert namespace.argument_original_value('foo') == 'bar'
argparse.Namespace.__setattr__(
namespace, '_%s__is_explicit' % type(namespace).__name__, True)
namespace.foo = 'baz'
assert str(namespace) == "ExplicitlySetNamespace(foo='baz')"
assert namespace.foo == 'baz'
assert namespace.argument_set_explicitly('foo')
assert namespace.argument_original_value('foo') == 'bar'
def test_DefaultCapturingNamespace_withParser():
parser = argparse.ArgumentParser()
parser.add_argument('foo', nargs='*')
parser.add_argument('--bar')
parser.add_argument('--baz', action='store_true')
parser.add_argument('--bang', default='biff')
parser.add_argument('--boom', default=argparse.SUPPRESS)
namespace = util.ExplicitlySetNamespace()
namespace.apply_monkey_patch(parser)
args = parser.parse_args(args=[], namespace=namespace)
# should all be assert not
assert args.argument_set_explicitly('foo')
assert not args.argument_set_explicitly('bar')
assert not args.argument_set_explicitly('baz')
assert args.argument_set_explicitly('bang')
assert not args.argument_set_explicitly('boom')
a=['foo', '--bar', 'bar', '--baz', '--bang', 'bang', '--boom', 'boom']
namespace = util.ExplicitlySetNamespace()
namespace.apply_monkey_patch(parser)
args = parser.parse_args(args=a, namespace=namespace)
assert args.argument_set_explicitly('foo')
assert args.argument_original_value('foo') is None
assert args.argument_set_explicitly('bar')
assert args.argument_original_value('bar') is None
assert args.argument_set_explicitly('baz')
assert args.argument_original_value('baz') == False
assert args.argument_set_explicitly('bang')
assert args.argument_original_value('bang') == 'biff'
assert args.argument_set_explicitly('boom')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment