diff options
author | Aldo Cortesi <aldo@corte.si> | 2018-05-08 10:56:00 +1200 |
---|---|---|
committer | Aldo Cortesi <aldo@corte.si> | 2018-05-08 10:56:00 +1200 |
commit | f7d7e31f06dadcf0f7801ab1db055f8609c88a71 (patch) | |
tree | 56aa748d814c00a594c8c46338d6e4abb1c9f629 | |
parent | 7ec9c5524f64bbceaf52b58696c8c9342a4887dd (diff) | |
download | mitmproxy-f7d7e31f06dadcf0f7801ab1db055f8609c88a71.tar.gz mitmproxy-f7d7e31f06dadcf0f7801ab1db055f8609c88a71.tar.bz2 mitmproxy-f7d7e31f06dadcf0f7801ab1db055f8609c88a71.zip |
options: add the concept of deferred settings
We've had a perpetual sequencing problem with addon startup. Users need to be
able to specify options to addons on the command-line, before addons are
actually loaded. This is only exacerbated with the new async core, where load
order can't be relied on.
This patch introduces deferred options. Options passed with "--set" on the
command line are deferred if they are unknown, and are automatically applied by
the addon manager once matching addons are registered and their options are defined.
-rw-r--r-- | mitmproxy/addonmanager.py | 1 | ||||
-rw-r--r-- | mitmproxy/optmanager.py | 64 | ||||
-rw-r--r-- | mitmproxy/tools/main.py | 2 | ||||
-rw-r--r-- | test/mitmproxy/test_optmanager.py | 11 |
4 files changed, 60 insertions, 18 deletions
diff --git a/mitmproxy/addonmanager.py b/mitmproxy/addonmanager.py index a9f6551f..64c957ba 100644 --- a/mitmproxy/addonmanager.py +++ b/mitmproxy/addonmanager.py @@ -173,6 +173,7 @@ class AddonManager: self.lookup[name] = a for a in traverse([addon]): self.master.commands.collect_commands(a) + self.master.options.process_deferred() return addon def add(self, *addons): diff --git a/mitmproxy/optmanager.py b/mitmproxy/optmanager.py index bb9e3030..7dc4bf6c 100644 --- a/mitmproxy/optmanager.py +++ b/mitmproxy/optmanager.py @@ -91,9 +91,12 @@ class OptManager: mutation doesn't change the option state inadvertently. """ def __init__(self): - self.__dict__["_options"] = {} - self.__dict__["changed"] = blinker.Signal() - self.__dict__["errored"] = blinker.Signal() + self._deferred: typing.Dict[str, str] = {} + self.changed = blinker.Signal() + self.errored = blinker.Signal() + # Options must be the last attribute here - after that, we raise an + # error for attribute assigment to unknown options. + self._options: typing.Dict[str, typing.Any] = {} def add_option( self, @@ -168,7 +171,14 @@ class OptManager: raise AttributeError("No such option: %s" % attr) def __setattr__(self, attr, value): - self.update(**{attr: value}) + # This is slightly tricky. We allow attributes to be set on the instance + # until we have an _options attribute. After that, assignment is sent to + # the update function, and will raise an error for unknown options. + opts = self.__dict__.get("_options") + if not opts: + super().__setattr__(attr, value) + else: + self.update(**{attr: value}) def keys(self): return set(self._options.keys()) @@ -272,12 +282,44 @@ class OptManager: options=options ) - def set(self, *spec): + def set(self, *spec, defer=False): + """ + Takes a list of set specification in standard form (option=value). + Options that are known are updated immediately. If defer is true, + options that are not known are deferred, and will be set once they + are added. + """ vals = {} + unknown = {} for i in spec: - vals.update(self._setspec(i)) + parts = i.split("=", maxsplit=1) + if len(parts) == 1: + optname, optval = parts[0], None + else: + optname, optval = parts[0], parts[1] + if optname in self._options: + vals[optname] = self.parse_setval(optname, optval) + else: + unknown[optname] = optval + if defer: + self._deferred.update(unknown) + elif unknown: + raise exceptions.OptionsError("Unknown options: %s" % ", ".join(unknown.keys())) self.update(**vals) + def process_deferred(self): + """ + Processes options that were deferred in previous calls to set, and + have since been added. + """ + update = {} + for optname, optval in self._deferred.items(): + if optname in self._options: + update[optname] = self.parse_setval(optname, optval) + self.update(**update) + for k in update.keys(): + del self._deferred[k] + def parse_setval(self, optname: str, optstr: typing.Optional[str]) -> typing.Any: """ Convert a string to a value appropriate for the option type. @@ -316,16 +358,6 @@ class OptManager: return getattr(self, optname) + [optstr] raise NotImplementedError("Unsupported option type: %s", o.typespec) - def _setspec(self, spec): - d = {} - parts = spec.split("=", maxsplit=1) - if len(parts) == 1: - optname, optval = parts[0], None - else: - optname, optval = parts[0], parts[1] - d[optname] = self.parse_setval(optname, optval) - return d - def make_parser(self, parser, optname, metavar=None, short=None): """ Auto-Create a command-line parser entry for a named option. If the diff --git a/mitmproxy/tools/main.py b/mitmproxy/tools/main.py index cf370f29..0b271b91 100644 --- a/mitmproxy/tools/main.py +++ b/mitmproxy/tools/main.py @@ -110,7 +110,7 @@ def run( if args.commands: master.commands.dump() sys.exit(0) - opts.set(*args.setoptions) + opts.set(*args.setoptions, defer=True) if extra: opts.update(**extra(args)) diff --git a/test/mitmproxy/test_optmanager.py b/test/mitmproxy/test_optmanager.py index 1c49c0b8..77bcc007 100644 --- a/test/mitmproxy/test_optmanager.py +++ b/test/mitmproxy/test_optmanager.py @@ -426,4 +426,13 @@ def test_set(): assert opts.seqstr == [] with pytest.raises(exceptions.OptionsError): - opts.set("nonexistent=wobble") + opts.set("deferred=wobble") + + opts.set("deferred=wobble", defer=True) + assert "deferred" in opts._deferred + opts.process_deferred() + assert "deferred" in opts._deferred + opts.add_option("deferred", str, "default", "help") + opts.process_deferred() + assert "deferred" not in opts._deferred + assert opts.deferred == "wobble" |