diff --git a/examples/simple/custom_option.py b/examples/simple/custom_option.py index a8b4e7789..324d27e7c 100644 --- a/examples/simple/custom_option.py +++ b/examples/simple/custom_option.py @@ -7,4 +7,5 @@ def start(options): 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) diff --git a/mitmproxy/optmanager.py b/mitmproxy/optmanager.py index 225a2f739..495354f4c 100644 --- a/mitmproxy/optmanager.py +++ b/mitmproxy/optmanager.py @@ -81,8 +81,6 @@ class _Option: class OptManager: """ 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 updated. If any handler in the chain raises an exceptions.OptionsError @@ -176,15 +174,29 @@ class OptManager: o.reset() 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): - updated = set(kwargs.keys()) - with self.rollback(updated): - for k, v in kwargs.items(): - 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 + u = self.update_known(**kwargs) + if u: + raise KeyError("Unknown options: %s" % ", ".join(u.keys())) def setter(self, attr): """ @@ -413,12 +425,11 @@ def load(opts, text): """ Load configuration from text, over-writing options already set in this object. May raise OptionsError if the config file is invalid. + + Returns a dictionary of all unknown options. """ data = parse(text) - try: - opts.update(**data) - except KeyError as v: - raise exceptions.OptionsError(v) + return opts.update_known(**data) 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 path. Paths that don't exist are ignored, errors raise an OptionsError. + + Returns a dictionary of unknown options. """ + ret = {} for p in paths: p = os.path.expanduser(p) if os.path.exists(p) and os.path.isfile(p): with open(p, "r") as f: txt = f.read() try: - load(opts, txt) + ret.update(load(opts, txt)) except exceptions.OptionsError as e: raise exceptions.OptionsError( "Error reading %s: %s" % (p, e) ) + return ret def serialize(opts, text, defaults=False): diff --git a/mitmproxy/tools/main.py b/mitmproxy/tools/main.py index 8210580cf..35567b620 100644 --- a/mitmproxy/tools/main.py +++ b/mitmproxy/tools/main.py @@ -69,10 +69,13 @@ def run(MasterKlass, args): # pragma: no cover args = parser.parse_args(args) master = None try: - optmanager.load_paths(opts, args.conf) + unknown = optmanager.load_paths(opts, args.conf) server = process_options(parser, opts, args) master = MasterKlass(opts, server) 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: print(optmanager.dump_defaults(opts)) sys.exit(0) diff --git a/test/mitmproxy/test_optmanager.py b/test/mitmproxy/test_optmanager.py index 012c463c9..df3928290 100644 --- a/test/mitmproxy/test_optmanager.py +++ b/test/mitmproxy/test_optmanager.py @@ -83,10 +83,11 @@ def test_options(): with pytest.raises(TypeError): TO(nonexistent = "value") - with pytest.raises(Exception, match="No such option"): + with pytest.raises(Exception, match="Unknown options"): o.nonexistent = "value" - with pytest.raises(Exception, match="No such option"): + with pytest.raises(Exception, match="Unknown options"): o.update(nonexistent = "value") + assert o.update_known(nonexistent = "value") == {"nonexistent": "value"} rec = [] @@ -226,9 +227,7 @@ def test_serialize(): t = "" optmanager.load(o2, t) - - with pytest.raises(exceptions.OptionsError, matches='No such option: foobar'): - optmanager.load(o2, "foobar: '123'") + assert optmanager.load(o2, "foobar: '123'") == {"foobar": "123"} def test_serialize_defaults(): @@ -252,7 +251,11 @@ def test_saving(tmpdir): with open(dst, 'a') as f: 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)