Make it possible to set sequence options (#4210)

* Make it possible to set sequence options

Attempts to fix #3015 through looking at whether or not the option is
of the type Sequence[str].

Treat all deferred options as potentially Sequence options, by making the
deferred dict values a list.

* Add full test coverage to optmanager again

* Document how to set sequence options

* minor improvements

* update changelog

Co-authored-by: Maximilian Hils <git@maximilianhils.com>
This commit is contained in:
Jesper Bränn 2021-06-23 20:08:24 +02:00 committed by GitHub
parent 7603987ee0
commit 64961232e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 32 additions and 10 deletions

View file

@ -67,6 +67,7 @@ Mitmproxy has a completely new proxy core, fixing many longstanding issues:
(@mhils)
* `json()` method for HTTP Request and Response instances will return decoded JSON body. (@rbdixon)
* Support for HTTP/2 Push Promises has been dropped. (@mhils)
* Make it possible to set sequence options from the command line (@Yopi)
* --- TODO: add new PRs above this line ---
* ... and various other fixes, documentation improvements, dependency version bumps, etc.

View file

@ -56,6 +56,8 @@ class Core:
strings and integers are set to None (if permitted), and sequences
are emptied. Boolean values can be true, false or toggle.
Multiple values are concatenated with a single space.
Sequences are set using multiple invocations to set for
the same option.
"""
strspec = f"{option}={value}"
try:

View file

@ -1,3 +1,4 @@
import collections
import contextlib
import blinker
import blinker._saferef
@ -91,7 +92,7 @@ class OptManager:
mutation doesn't change the option state inadvertently.
"""
def __init__(self):
self.deferred: typing.Dict[str, str] = {}
self.deferred: typing.Dict[str, typing.List[str]] = {}
self.changed = blinker.Signal()
self.errored = blinker.Signal()
# Options must be the last attribute here - after that, we raise an
@ -295,7 +296,7 @@ class OptManager:
are added.
"""
vals = {}
unknown = {}
unknown: typing.Dict[str, typing.List[str]] = collections.defaultdict(list)
for i in spec:
parts = i.split("=", maxsplit=1)
if len(parts) == 1:
@ -303,9 +304,9 @@ class OptManager:
else:
optname, optval = parts[0], parts[1]
if optname in self._options:
vals[optname] = self.parse_setval(self._options[optname], optval)
vals[optname] = self.parse_setval(self._options[optname], optval, vals.get(optname))
else:
unknown[optname] = optval
unknown[optname].append(optval)
if defer:
self.deferred.update(unknown)
elif unknown:
@ -318,15 +319,16 @@ class OptManager:
have since been added.
"""
update = {}
for optname, optval in self.deferred.items():
for optname, optvals in self.deferred.items():
if optname in self._options:
optval = self.parse_setval(self._options[optname], optval)
update[optname] = optval
for optval in optvals:
optval = self.parse_setval(self._options[optname], optval, update.get(optname))
update[optname] = optval
self.update(**update)
for k in update.keys():
del self.deferred[k]
def parse_setval(self, o: _Option, optstr: typing.Optional[str]) -> typing.Any:
def parse_setval(self, o: _Option, optstr: typing.Optional[str], currentvalue: typing.Any) -> typing.Any:
"""
Convert a string to a value appropriate for the option type.
"""
@ -357,7 +359,10 @@ class OptManager:
if not optstr:
return []
else:
return getattr(self, o.name) + [optstr]
if currentvalue:
return getattr(self, o.name) + currentvalue + [optstr]
else:
return getattr(self, o.name) + [optstr]
raise NotImplementedError("Unsupported option type: %s", o.typespec)
def make_parser(self, parser, optname, metavar=None, short=None):

View file

@ -27,6 +27,8 @@ def common_options(parser, opts):
Set an option. When the value is omitted, booleans are set to true,
strings and integers are set to None (if permitted), and sequences
are emptied. Boolean values can be true, false or toggle.
Sequences are set using multiple invocations to set for
the same option.
"""
)
parser.add_argument(

View file

@ -71,7 +71,7 @@ def test_defaults():
def test_required_int():
o = TO()
with pytest.raises(exceptions.OptionsError):
o.parse_setval(o._options["required_int"], None)
o.parse_setval(o._options["required_int"], None, None)
def test_deepcopy():
@ -439,6 +439,9 @@ def test_set():
opts.set("seqstr")
assert opts.seqstr == []
opts.set(*('seqstr=foo', 'seqstr=bar'))
assert opts.seqstr == ["foo", "bar"]
with pytest.raises(exceptions.OptionsError):
opts.set("deferredoption=wobble")
@ -450,3 +453,12 @@ def test_set():
opts.process_deferred()
assert "deferredoption" not in opts.deferred
assert opts.deferredoption == "wobble"
opts.set(*('deferredsequenceoption=a', 'deferredsequenceoption=b'), defer=True)
assert "deferredsequenceoption" in opts.deferred
opts.process_deferred()
assert "deferredsequenceoption" in opts.deferred
opts.add_option("deferredsequenceoption", typing.Sequence[str], [], "help")
opts.process_deferred()
assert "deferredsequenceoption" not in opts.deferred
assert opts.deferredsequenceoption == ["a", "b"]