Add YAML serialization of options

This uses ruamel.yaml. The library seems well-supported, and can do in-place
modification of config files that retains user comments and file structure.
This commit is contained in:
Aldo Cortesi 2016-12-05 07:18:53 +13:00
parent c94cd512d1
commit d74cac265a
3 changed files with 125 additions and 1 deletions

View File

@ -5,6 +5,9 @@ import inspect
import copy import copy
import functools import functools
import weakref import weakref
import os
import ruamel.yaml
from mitmproxy import exceptions from mitmproxy import exceptions
from mitmproxy.utils import typecheck from mitmproxy.utils import typecheck
@ -171,6 +174,73 @@ class OptManager(metaclass=_DefaultsMeta):
if getattr(self, option) != self._defaults[option]: if getattr(self, option) != self._defaults[option]:
return True return True
def save(self, path, defaults=False):
"""
Save to path. If the destination file exists, modify it in-place.
"""
if os.path.exists(path) and os.path.isfile(path):
data = open(path, "r").read()
else:
data = ""
data = self.serialize(data, defaults)
fp = open(path, "w")
fp.write(data)
def serialize(self, text, defaults=False):
"""
Performs a round-trip serialization. If text is not None, it is
treated as a previous serialization that should be modified
in-place.
- If "defaults" is False, only options with non-default values are
serialized. Default values in text are preserved.
- Unknown options in text are removed.
- Raises OptionsError if text is invalid.
"""
data = self._load(text)
for k in self.keys():
if defaults or self.has_changed(k):
data[k] = getattr(self, k)
for k in list(data.keys()):
if k not in self._opts:
del data[k]
return ruamel.yaml.round_trip_dump(data)
def _load(self, text):
if not text:
return {}
try:
data = ruamel.yaml.load(text, ruamel.yaml.Loader)
except ruamel.yaml.error.YAMLError as v:
snip = v.problem_mark.get_snippet()
raise exceptions.OptionsError(
"Config error at line %s:\n%s\n%s" %
(v.problem_mark.line+1, snip, v.problem)
)
if isinstance(data, str):
raise exceptions.OptionsError("Config error - no keys found.")
return data
def load(self, text):
"""
Load configuration from text, over-writing options already set in
this object. May raise OptionsError if the config file is invalid.
"""
data = self._load(text)
for k, v in data.items():
setattr(self, k, v)
def load_paths(self, *paths):
"""
Load paths in order. Each path takes precedence over the previous
path. Paths that don't exist are ignored, errors raise an
OptionsError.
"""
for p in paths:
if os.path.exists(p) and os.path.isfile(p):
txt = open(p, "r").read()
self.load(txt)
def __repr__(self): def __repr__(self):
options = pprint.pformat(self._opts, indent=4).strip(" {}") options = pprint.pformat(self._opts, indent=4).strip(" {}")
if "\n" in options: if "\n" in options:

View File

@ -79,6 +79,7 @@ setup(
"pyparsing>=2.1.3, <2.2", "pyparsing>=2.1.3, <2.2",
"pyperclip>=1.5.22, <1.6", "pyperclip>=1.5.22, <1.6",
"requests>=2.9.1, <3", "requests>=2.9.1, <3",
"ruamel.yaml>=0.13.2, <0.14",
"tornado>=4.3, <4.5", "tornado>=4.3, <4.5",
"urwid>=1.3.1, <1.4", "urwid>=1.3.1, <1.4",
"watchdog>=0.8.3, <0.9", "watchdog>=0.8.3, <0.9",

View File

@ -1,5 +1,7 @@
import copy import copy
import os
from mitmproxy import options
from mitmproxy import optmanager from mitmproxy import optmanager
from mitmproxy import exceptions from mitmproxy import exceptions
from mitmproxy.test import tutils from mitmproxy.test import tutils
@ -24,7 +26,7 @@ class TD2(TD):
def __init__(self, *, three="dthree", four="dfour", **kwargs): def __init__(self, *, three="dthree", four="dfour", **kwargs):
self.three = three self.three = three
self.four = four self.four = four
super().__init__(**kwargs) super().__init__(three=three, **kwargs)
def test_defaults(): def test_defaults():
@ -167,3 +169,54 @@ def test_repr():
'one': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'one': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'two': None 'two': None
})""" })"""
def test_serialize():
o = TD2()
o.three = "set"
assert "dfour" in o.serialize(None, defaults=True)
data = o.serialize(None)
assert "dfour" not in data
o2 = TD2()
o2.load(data)
assert o2 == o
t = """
unknown: foo
"""
data = o.serialize(t)
o2 = TD2()
o2.load(data)
assert o2 == o
t = "invalid: foo\ninvalid"
tutils.raises("config error", o2.load, t)
t = "invalid"
tutils.raises("config error", o2.load, t)
t = ""
o2.load(t)
def test_serialize_defaults():
o = options.Options()
assert o.serialize(None, defaults=True)
def test_saving():
o = TD2()
o.three = "set"
with tutils.tmpdir() as tdir:
dst = os.path.join(tdir, "conf")
o.save(dst, defaults=True)
o2 = TD2()
o2.load_paths(dst)
o2.three = "foo"
o2.save(dst, defaults=True)
o.load_paths(dst)
assert o.three == "foo"