Created
March 5, 2020 05:00
-
-
Save dimo414/b36b1622f751b578df06512957159f07 to your computer and use it in GitHub Desktop.
Detecting whether an argparse argument was set explicitly or not
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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