Enable custom options in config files

We also now ignore unknown options in config files by default, and print a
warning if verbosity is incremented.
This commit is contained in:
Aldo Cortesi 2017-03-14 09:22:44 +13:00
parent 2832e790fd
commit b745428b5c
4 changed files with 45 additions and 23 deletions

View File

@ -7,4 +7,5 @@ def start(options):
def configure(options, updated): def configure(options, updated):
ctx.log.info("custom option value: %s" % options.custom) if "custom" in updated:
ctx.log.info("custom option value: %s" % options.custom)

View File

@ -81,8 +81,6 @@ class _Option:
class OptManager: class OptManager:
""" """
OptManager is the base class from which Options objects are derived. OptManager is the base class from which Options objects are derived.
Note that the __init__ method of all child classes must force all
arguments to be positional only, by including a "*" argument.
.changed is a blinker Signal that triggers whenever options are .changed is a blinker Signal that triggers whenever options are
updated. If any handler in the chain raises an exceptions.OptionsError updated. If any handler in the chain raises an exceptions.OptionsError
@ -176,15 +174,29 @@ class OptManager:
o.reset() o.reset()
self.changed.send(self._options.keys()) self.changed.send(self._options.keys())
def update_known(self, **kwargs):
"""
Update and set all known options from kwargs. Returns a dictionary
of unknown options.
"""
known, unknown = {}, {}
for k, v in kwargs.items():
if k in self._options:
known[k] = v
else:
unknown[k] = v
updated = set(known.keys())
if updated:
with self.rollback(updated):
for k, v in known.items():
self._options[k].set(v)
self.changed.send(self, updated=updated)
return unknown
def update(self, **kwargs): def update(self, **kwargs):
updated = set(kwargs.keys()) u = self.update_known(**kwargs)
with self.rollback(updated): if u:
for k, v in kwargs.items(): raise KeyError("Unknown options: %s" % ", ".join(u.keys()))
if k not in self._options:
raise KeyError("No such option: %s" % k)
self._options[k].set(v)
self.changed.send(self, updated=updated)
return self
def setter(self, attr): def setter(self, attr):
""" """
@ -413,12 +425,11 @@ def load(opts, text):
""" """
Load configuration from text, over-writing options already set in Load configuration from text, over-writing options already set in
this object. May raise OptionsError if the config file is invalid. this object. May raise OptionsError if the config file is invalid.
Returns a dictionary of all unknown options.
""" """
data = parse(text) data = parse(text)
try: return opts.update_known(**data)
opts.update(**data)
except KeyError as v:
raise exceptions.OptionsError(v)
def load_paths(opts, *paths): def load_paths(opts, *paths):
@ -426,18 +437,22 @@ def load_paths(opts, *paths):
Load paths in order. Each path takes precedence over the previous Load paths in order. Each path takes precedence over the previous
path. Paths that don't exist are ignored, errors raise an path. Paths that don't exist are ignored, errors raise an
OptionsError. OptionsError.
Returns a dictionary of unknown options.
""" """
ret = {}
for p in paths: for p in paths:
p = os.path.expanduser(p) p = os.path.expanduser(p)
if os.path.exists(p) and os.path.isfile(p): if os.path.exists(p) and os.path.isfile(p):
with open(p, "r") as f: with open(p, "r") as f:
txt = f.read() txt = f.read()
try: try:
load(opts, txt) ret.update(load(opts, txt))
except exceptions.OptionsError as e: except exceptions.OptionsError as e:
raise exceptions.OptionsError( raise exceptions.OptionsError(
"Error reading %s: %s" % (p, e) "Error reading %s: %s" % (p, e)
) )
return ret
def serialize(opts, text, defaults=False): def serialize(opts, text, defaults=False):

View File

@ -69,10 +69,13 @@ def run(MasterKlass, args): # pragma: no cover
args = parser.parse_args(args) args = parser.parse_args(args)
master = None master = None
try: try:
optmanager.load_paths(opts, args.conf) unknown = optmanager.load_paths(opts, args.conf)
server = process_options(parser, opts, args) server = process_options(parser, opts, args)
master = MasterKlass(opts, server) master = MasterKlass(opts, server)
master.addons.configure_all(opts, opts.keys()) master.addons.configure_all(opts, opts.keys())
remaining = opts.update_known(**unknown)
if remaining and opts.verbosity > 1:
print("Ignored options: %s" % remaining)
if args.options: if args.options:
print(optmanager.dump_defaults(opts)) print(optmanager.dump_defaults(opts))
sys.exit(0) sys.exit(0)

View File

@ -83,10 +83,11 @@ def test_options():
with pytest.raises(TypeError): with pytest.raises(TypeError):
TO(nonexistent = "value") TO(nonexistent = "value")
with pytest.raises(Exception, match="No such option"): with pytest.raises(Exception, match="Unknown options"):
o.nonexistent = "value" o.nonexistent = "value"
with pytest.raises(Exception, match="No such option"): with pytest.raises(Exception, match="Unknown options"):
o.update(nonexistent = "value") o.update(nonexistent = "value")
assert o.update_known(nonexistent = "value") == {"nonexistent": "value"}
rec = [] rec = []
@ -226,9 +227,7 @@ def test_serialize():
t = "" t = ""
optmanager.load(o2, t) optmanager.load(o2, t)
assert optmanager.load(o2, "foobar: '123'") == {"foobar": "123"}
with pytest.raises(exceptions.OptionsError, matches='No such option: foobar'):
optmanager.load(o2, "foobar: '123'")
def test_serialize_defaults(): def test_serialize_defaults():
@ -252,7 +251,11 @@ def test_saving(tmpdir):
with open(dst, 'a') as f: with open(dst, 'a') as f:
f.write("foobar: '123'") f.write("foobar: '123'")
with pytest.raises(exceptions.OptionsError, matches=''): assert optmanager.load_paths(o, dst) == {"foobar": "123"}
with open(dst, 'a') as f:
f.write("'''")
with pytest.raises(exceptions.OptionsError):
optmanager.load_paths(o, dst) optmanager.load_paths(o, dst)