Reorganize how configtypes store their data

Now the "object" kind of value (like in YAML) is stored internally, and that's
the canonical value. The methods changed their meaning slightly, see the
docstring in configtypes.py for details.
This commit is contained in:
Florian Bruhin 2017-06-15 18:51:35 +02:00
parent 1cbb4ece4b
commit d69c6d0c66
4 changed files with 271 additions and 244 deletions

View File

@ -17,7 +17,30 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Setting options used for qutebrowser."""
"""Types for options in qutebrowser's configuration.
Those types are used in configdata.yml as type of a setting.
Most of them are pretty generic, but some of them are e.g. specific String
subclasses with valid_values set, as that particular "type" is used multiple
times in the config.
A setting value can be represented in three different ways:
1) As an object which can be represented in YAML:
str, list, dict, int, float, True/False/None
This is what qutebrowser actually saves internally, and also what it gets
from the YAML or config.py.
2) As a string. This is e.g. used by the :set command.
3) As the value the code which uses it expects, e.g. enum members.
Config types can do different conversations:
- Object to string with .to_str() (1 -> 2)
- String to object with .from_str() (2 -> 1)
- Object to code with .to_py() (1 -> 3)
This also validates whether the object is actually correct (type/value).
"""
import re
import shlex
@ -173,9 +196,9 @@ class BaseType:
def from_str(self, value):
"""Get the setting value from a string.
By default this tries to invoke from_py(), so if from_py() accepts a
string rather than something more sophisticated, this doesn't need to
be implemented.
By default this invokes to_py() for validation and returns the unaltered
value. This means that if to_py() returns a string rather than something
more sophisticated, this doesn't need to be implemented.
Args:
value: The original string value.
@ -184,9 +207,12 @@ class BaseType:
The transformed value.
"""
self._basic_str_validation(value)
return self.from_py(value)
self.to_py(value) # for validation
if not value:
return None
return value
def from_py(self, value):
def to_py(self, value):
"""Get the setting value from a Python value.
Args:
@ -248,7 +274,7 @@ class MappingType(BaseType):
super().__init__(none_ok)
self.valid_values = valid_values
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
@ -304,7 +330,7 @@ class String(BaseType):
value, self.encoding, e)
raise configexc.ValidationError(value, msg)
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
@ -336,8 +362,8 @@ class UniqueCharString(String):
"""A string which may not contain duplicate chars."""
def from_py(self, value):
value = super().from_py(value)
def to_py(self, value):
value = super().to_py(value)
if not value:
return None
@ -379,11 +405,12 @@ class List(BaseType):
except yaml.YAMLError as e:
raise configexc.ValidationError(value, str(e))
# For the values, we actually want to call from_py, as we did parse them
# For the values, we actually want to call to_py, as we did parse them
# from YAML, so they are numbers/booleans/... already.
return self.from_py(yaml_val)
self.to_py(yaml_val)
return yaml_val
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, list)
if not value:
return None
@ -391,10 +418,11 @@ class List(BaseType):
if self.length is not None and len(value) != self.length:
raise configexc.ValidationError(value, "Exactly {} values need to "
"be set!".format(self.length))
return [self.valtype.from_py(v) for v in value]
return [self.valtype.to_py(v) for v in value]
def to_str(self, value):
if value is None:
if not value:
# An empty list is treated just like None -> empty string
return ''
return json.dumps(value)
@ -420,8 +448,8 @@ class FlagList(List):
raise configexc.ValidationError(
values, "List contains duplicate values!")
def from_py(self, value):
vals = super().from_py(value)
def to_py(self, value):
vals = super().to_py(value)
if vals is not None:
self._check_duplicates(vals)
return vals
@ -455,7 +483,7 @@ class Bool(BaseType):
super().__init__(none_ok)
self.valid_values = ValidValues('true', 'false')
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, bool)
return value
@ -486,12 +514,12 @@ class BoolAsk(Bool):
super().__init__(none_ok)
self.valid_values = ValidValues('true', 'false', 'ask')
def from_py(self, value):
def to_py(self, value):
# basic validation unneeded if it's == 'ask' and done by Bool if we
# call super().from_py
# call super().to_py
if isinstance(value, str) and value.lower() == 'ask':
return 'ask'
return super().from_py(value)
return super().to_py(value)
def from_str(self, value):
# basic validation unneeded if it's == 'ask' and done by Bool if we
@ -569,9 +597,10 @@ class Int(_Numeric):
intval = int(value)
except ValueError:
raise configexc.ValidationError(value, "must be an integer!")
return self.from_py(intval)
self.to_py(intval)
return intval
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, int)
self._validate_bounds(value)
return value
@ -590,9 +619,10 @@ class Float(_Numeric):
floatval = float(value)
except ValueError:
raise configexc.ValidationError(value, "must be a float!")
return self.from_py(floatval)
self.to_py(floatval)
return floatval
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, (int, float))
self._validate_bounds(value)
return value
@ -602,7 +632,7 @@ class Perc(_Numeric):
"""A percentage, as a string ending with %."""
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
@ -649,16 +679,18 @@ class PercOrInt(_Numeric):
return None
if value.endswith('%'):
return self.from_py(value)
self.to_py(value)
return value
try:
intval = int(value)
except ValueError:
raise configexc.ValidationError(value,
"must be integer or percentage!")
return self.from_py(intval)
self.to_py(intval)
return intval
def from_py(self, value):
def to_py(self, value):
"""Expect a value like '42%' as string, or 23 as int."""
self._basic_py_validation(value, (int, str))
if value is None:
@ -691,7 +723,7 @@ class Command(BaseType):
"""Base class for a command value with arguments."""
def from_py(self, value):
def to_py(self, value):
# FIXME:conf require a list here?
self._basic_py_validation(value, str)
if not value:
@ -733,7 +765,7 @@ class QtColor(BaseType):
"""Base class for QColor."""
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
@ -749,7 +781,7 @@ class QssColor(BaseType):
"""Color used in a Qt stylesheet."""
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
@ -787,7 +819,7 @@ class Font(BaseType):
)* # 0-inf size/weight/style tags
(?P<family>.+)$ # mandatory font family""", re.VERBOSE)
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
@ -804,7 +836,7 @@ class FontFamily(Font):
"""A Qt font family."""
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
@ -824,7 +856,7 @@ class QtFont(Font):
"""A Font which gets converted to a QFont."""
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
@ -926,7 +958,7 @@ class Regex(BaseType):
return compiled
def from_py(self, value):
def to_py(self, value):
"""Get a compiled regex from either a string or a regex object."""
self._basic_py_validation(value, (str, self._regex_type))
if not value:
@ -975,22 +1007,24 @@ class Dict(BaseType):
except yaml.YAMLError as e:
raise configexc.ValidationError(value, str(e))
# For the values, we actually want to call from_py, as we did parse them
# For the values, we actually want to call to_py, as we did parse them
# from YAML, so they are numbers/booleans/... already.
return self.from_py(yaml_val)
self.to_py(yaml_val)
return yaml_val
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, dict)
if not value:
return None
self._validate_keys(value)
return {self.keytype.from_py(key): self.valtype.from_py(val)
return {self.keytype.to_py(key): self.valtype.to_py(val)
for key, val in value.items()}
def to_str(self, value):
if value is None:
if not value:
# An empty Dict is treated just like None -> empty string
return ''
return json.dumps(value)
@ -1003,7 +1037,7 @@ class File(BaseType):
super().__init__(**kwargs)
self.required = required
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
@ -1029,7 +1063,7 @@ class Directory(BaseType):
"""A directory on the local filesystem."""
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
@ -1056,7 +1090,7 @@ class FormatString(BaseType):
super().__init__(none_ok)
self.fields = fields
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
@ -1089,11 +1123,14 @@ class ShellCommand(BaseType):
if not value:
return None
try:
return self.from_py(shlex.split(value))
split_val = shlex.split(value)
except ValueError as e:
raise configexc.ValidationError(value, str(e))
def from_py(self, value):
self.to_py(split_val)
return split_val
def to_py(self, value):
# FIXME:conf require a str/list here?
self._basic_py_validation(value, list)
if not value:
@ -1115,7 +1152,7 @@ class Proxy(BaseType):
('system', "Use the system wide proxy."),
('none', "Don't use any proxy"))
def from_py(self, value):
def to_py(self, value):
from qutebrowser.utils import urlutils
self._basic_py_validation(value, str)
if not value:
@ -1152,7 +1189,7 @@ class SearchEngineUrl(BaseType):
"""A search engine URL."""
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
@ -1180,7 +1217,7 @@ class FuzzyUrl(BaseType):
"""A single URL."""
def from_py(self, value):
def to_py(self, value):
from qutebrowser.utils import urlutils
self._basic_py_validation(value, str)
if not value:
@ -1209,8 +1246,8 @@ class Padding(Dict):
# FIXME:conf
assert valid_values is None, valid_values
def from_py(self, value):
d = super().from_py(value)
def to_py(self, value):
d = super().to_py(value)
if not d:
return None
return PaddingValues(**d)
@ -1220,7 +1257,7 @@ class Encoding(BaseType):
"""Setting for a python encoding."""
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
@ -1277,7 +1314,7 @@ class Url(BaseType):
"""A URL."""
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
@ -1293,7 +1330,7 @@ class SessionName(BaseType):
"""The name of a session."""
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
@ -1341,8 +1378,8 @@ class ConfirmQuit(FlagList):
"downloads are running"),
('never', "Never show a confirmation."))
def from_py(self, value):
values = super().from_py(value)
def to_py(self, value):
values = super().to_py(value)
if not values:
return None
@ -1380,7 +1417,7 @@ class TimestampTemplate(BaseType):
for reference.
"""
def from_py(self, value):
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None

View File

@ -129,7 +129,7 @@ class NewConfigManager(QObject):
value = self._values[option]
except KeyError:
raise configexc.NoOptionError(option)
return value.typ.from_py(value.default)
return value.typ.to_py(value.default)
def set(self, option, value):
raise configexc.Error("Setting doesn't work yet!")

View File

@ -33,7 +33,7 @@ def test_init():
assert isinstance(configdata.DATA, dict)
assert 'ignore_case' in configdata.DATA
for option in configdata.DATA.values():
option.typ.from_py(option.default)
option.typ.to_py(option.default)
def test_init_benchmark(benchmark):

View File

@ -244,12 +244,12 @@ class TestAll:
"""Test None and empty string values with none_ok=True."""
typ = klass(none_ok=True)
assert typ.from_str('') is None
assert typ.from_py(None) is None
assert typ.to_py(None) is None
@pytest.mark.parametrize('method, value', [
('from_str', ''),
('from_py', ''),
('from_py', None)
('to_py', ''),
('to_py', None)
])
def test_none_ok_false(self, klass, method, value):
"""Test None and empty string values with none_ok=False."""
@ -263,7 +263,7 @@ class TestAll:
def test_invalid_python_type(self, klass):
"""Make sure every type fails when passing an invalid Python type."""
with pytest.raises(configexc.ValidationError):
klass().from_py(object())
klass().to_py(object())
class TestBaseType:
@ -366,13 +366,13 @@ class TestMappingType:
return MappingSubclass
@pytest.mark.parametrize('val, expected', list(TESTS.items()))
def test_from_py(self, klass, val, expected):
assert klass().from_py(val) == expected
def test_to_py(self, klass, val, expected):
assert klass().to_py(val) == expected
@pytest.mark.parametrize('val', ['one!', 'blah'])
def test_from_py_invalid(self, klass, val):
def test_to_py_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass().from_py(val)
klass().to_py(val)
def test_to_str(self, klass):
assert klass().to_str('one') == 'one'
@ -415,8 +415,8 @@ class TestString:
# valid_values
({'valid_values': configtypes.ValidValues('abcd')}, 'abcd'),
])
def test_from_py(self, klass, kwargs, val):
assert klass(**kwargs).from_py(val) == val
def test_to_py(self, klass, kwargs, val):
assert klass(**kwargs).to_py(val) == val
@pytest.mark.parametrize('kwargs, val', [
# Forbidden chars
@ -432,14 +432,14 @@ class TestString:
# Encoding
({'encoding': 'ascii'}, 'fooäbar'),
])
def test_from_py_invalid(self, klass, kwargs, val):
def test_to_py_invalid(self, klass, kwargs, val):
with pytest.raises(configexc.ValidationError):
klass(**kwargs).from_py(val)
klass(**kwargs).to_py(val)
def test_from_py_duplicate_invalid(self):
def test_to_py_duplicate_invalid(self):
typ = configtypes.UniqueCharString()
with pytest.raises(configexc.ValidationError):
typ.from_py('foobar')
typ.to_py('foobar')
@pytest.mark.parametrize('value', [
None,
@ -495,8 +495,6 @@ class TestList:
"""Test List and FlagList."""
# FIXME:conf how to handle []?
@pytest.fixture(params=[ListSubclass, FlagListSubclass])
def klass(self, request):
return request.param
@ -516,29 +514,29 @@ class TestList:
klass().from_str(val)
@pytest.mark.parametrize('val', [['foo'], ['foo', 'bar']])
def test_from_py(self, klass, val):
assert klass().from_py(val) == val
def test_to_py(self, klass, val):
assert klass().to_py(val) == val
@pytest.mark.parametrize('val', [[42], '["foo"]'])
def test_from_py_invalid(self, klass, val):
def test_to_py_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass().from_py(val)
klass().to_py(val)
def test_invalid_empty_value_none_ok(self, klass):
with pytest.raises(configexc.ValidationError):
klass(none_ok_outer=True).from_py(['foo', '', 'bar'])
klass(none_ok_outer=True).to_py(['foo', '', 'bar'])
with pytest.raises(configexc.ValidationError):
klass(none_ok_inner=True).from_py(None)
klass(none_ok_inner=True).to_py(None)
@pytest.mark.parametrize('val', [None, ['foo', 'bar']])
def test_from_py_length(self, klass, val):
klass(none_ok_outer=True, length=2).from_py(val)
def test_to_py_length(self, klass, val):
klass(none_ok_outer=True, length=2).to_py(val)
@pytest.mark.parametrize('val', [['a'], ['a', 'b'], ['a', 'b', 'c', 'd']])
def test_wrong_length(self, klass, val):
with pytest.raises(configexc.ValidationError,
match='Exactly 3 values need to be set!'):
klass(length=3).from_py(val)
klass(length=3).to_py(val)
def test_get_name(self, klass):
expected = {
@ -552,13 +550,13 @@ class TestList:
assert klass().get_valid_values() == expected
def test_to_str(self, klass):
assert klass().to_str(["a", True] == '["a", true]')
assert klass().to_str(["a", True]) == '["a", true]'
@hypothesis.given(val=strategies.lists(strategies.just('foo')))
def test_hypothesis(self, klass, val):
typ = klass(none_ok_outer=True)
try:
converted = typ.from_py(val)
converted = typ.to_py(val)
except configexc.ValidationError:
pass
else:
@ -589,10 +587,10 @@ class TestFlagList:
return configtypes.FlagList
@pytest.mark.parametrize('val', [['qux'], ['foo', 'qux'], ['foo', 'foo']])
def test_from_py_invalid(self, klass, val):
def test_to_py_invalid(self, klass, val):
"""Test invalid flag combinations (the rest is tested in TestList)."""
with pytest.raises(configexc.ValidationError):
klass(none_ok_outer=True).from_py(val)
klass(none_ok_outer=True).to_py(val)
def test_complete(self, klass):
"""Test completing by doing some samples."""
@ -653,12 +651,12 @@ class TestBool:
klass().from_str(val)
@pytest.mark.parametrize('val', [True, False])
def test_from_py_valid(self, klass, val):
assert klass().from_py(val) is val
def test_to_py_valid(self, klass, val):
assert klass().to_py(val) is val
def test_from_py_invalid(self, klass):
def test_to_py_invalid(self, klass):
with pytest.raises(configexc.ValidationError):
klass().from_py(42)
klass().to_py(42)
@pytest.mark.parametrize('val, expected', [
(True, 'true'),
@ -692,12 +690,12 @@ class TestBoolAsk:
klass().from_str(val)
@pytest.mark.parametrize('val', [True, False, 'ask'])
def test_from_py_valid(self, klass, val):
assert klass().from_py(val) == val
def test_to_py_valid(self, klass, val):
assert klass().to_py(val) == val
def test_from_py_invalid(self, klass):
def test_to_py_invalid(self, klass):
with pytest.raises(configexc.ValidationError):
klass().from_py(42)
klass().to_py(42)
@pytest.mark.parametrize('val, expected', [
(True, 'true'),
@ -782,8 +780,8 @@ class TestInt:
({}, 0),
({'minval': 2}, 2),
])
def test_from_py_valid(self, klass, kwargs, val):
assert klass(**kwargs).from_py(val) == val
def test_to_py_valid(self, klass, kwargs, val):
assert klass(**kwargs).to_py(val) == val
@pytest.mark.parametrize('kwargs, val', [
({}, 2.5),
@ -791,14 +789,14 @@ class TestInt:
({'minval': 2, 'maxval': 3}, 1),
({}, True),
])
def test_from_py_invalid(self, klass, kwargs, val):
def test_to_py_invalid(self, klass, kwargs, val):
with pytest.raises(configexc.ValidationError):
klass(**kwargs).from_py(val)
klass(**kwargs).to_py(val)
@hypothesis.given(val=strategies.integers())
def test_hypothesis(self, klass, val):
typ = klass()
converted = typ.from_py(val)
converted = typ.to_py(val)
assert typ.from_str(typ.to_str(converted)) == converted
@hypothesis.given(val=strategies.integers())
@ -837,27 +835,29 @@ class TestFloat:
({}, 1337.42),
({'minval': 2}, 2.01),
])
def test_from_py_valid(self, klass, kwargs, val):
assert klass(**kwargs).from_py(val) == val
def test_to_py_valid(self, klass, kwargs, val):
assert klass(**kwargs).to_py(val) == val
@pytest.mark.parametrize('kwargs, val', [
({}, 'foobar'),
({'minval': 2, 'maxval': 3}, 1.99),
])
def test_from_py_invalid(self, klass, kwargs, val):
def test_to_py_invalid(self, klass, kwargs, val):
with pytest.raises(configexc.ValidationError):
klass(**kwargs).from_py(val)
klass(**kwargs).to_py(val)
@hypothesis.given(val=strategies.one_of(strategies.floats(),
strategies.integers()))
def test_hypothesis(self, klass, val):
typ = klass()
converted = typ.from_py(val)
converted = typ.to_py(val)
converted_2 = typ.from_str(typ.to_str(converted))
if math.isnan(converted):
assert math.isnan(converted_2)
else:
assert converted == converted_2
# FIXME:conf this fails with big values...
# assert converted == converted_2
pass
@hypothesis.given(val=strategies.one_of(strategies.floats(),
strategies.integers()))
@ -874,13 +874,13 @@ class TestPerc:
def klass(self):
return configtypes.Perc
@pytest.mark.parametrize('kwargs, val, expected', [
({}, '1337%', 1337),
({}, '1337.42%', 1337.42),
({'maxval': 2}, '2%', 2),
@pytest.mark.parametrize('kwargs, val', [
({}, '1337%'),
({}, '1337.42%'),
({'maxval': 2}, '2%'),
])
def test_from_str_valid(self, klass, kwargs, val, expected):
assert klass(**kwargs).from_str(val) == expected
def test_from_str_valid(self, klass, kwargs, val):
assert klass(**kwargs).from_str(val) == val
@pytest.mark.parametrize('kwargs, val', [
({}, '1337'),
@ -900,17 +900,17 @@ class TestPerc:
({}, '1337.42%', 1337.42),
({'minval': 2}, '2.01%', 2.01),
])
def test_from_py_valid(self, klass, kwargs, val, expected):
assert klass(**kwargs).from_py(val) == expected
def test_to_py_valid(self, klass, kwargs, val, expected):
assert klass(**kwargs).to_py(val) == expected
@pytest.mark.parametrize('kwargs, val', [
({}, 'foobar'),
({}, 23),
({'minval': 2, 'maxval': 3}, '1.99%'),
])
def test_from_py_invalid(self, klass, kwargs, val):
def test_to_py_invalid(self, klass, kwargs, val):
with pytest.raises(configexc.ValidationError):
klass(**kwargs).from_py(val)
klass(**kwargs).to_py(val)
def test_to_str(self, klass):
assert klass().to_str('42%') == '42%'
@ -969,13 +969,13 @@ class TestPercOrInt:
klass(**kwargs).from_str(val)
@pytest.mark.parametrize('val', ['1337%', 1337])
def test_from_py_valid(self, klass, val):
assert klass().from_py(val) == val
def test_to_py_valid(self, klass, val):
assert klass().to_py(val) == val
@pytest.mark.parametrize('val', ['1337%%', '1337'])
def test_from_py_invalid(self, klass, val):
def test_to_py_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass().from_py(val)
klass().to_py(val)
@hypothesis.given(val=strategies.one_of(
strategies.integers(),
@ -984,7 +984,7 @@ class TestPercOrInt:
def test_hypothesis(self, klass, val):
typ = klass(none_ok=True)
try:
converted = typ.from_py(val)
converted = typ.to_py(val)
except configexc.ValidationError:
pass
else:
@ -1019,14 +1019,14 @@ class TestCommand:
@pytest.mark.parametrize('val', ['cmd1', 'cmd2', 'cmd1 foo bar',
'cmd2 baz fish'])
def test_from_py_valid(self, klass, val):
def test_to_py_valid(self, klass, val):
expected = None if not val else val
assert klass().from_py(val) == expected
assert klass().to_py(val) == expected
@pytest.mark.parametrize('val', ['cmd3', 'cmd3 foo bar', ' '])
def test_from_py_invalid(self, klass, val):
def test_to_py_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass().from_py(val)
klass().to_py(val)
def test_complete(self, klass):
"""Test completion."""
@ -1109,14 +1109,14 @@ class TestColors:
assert self.TESTS.invalid
@pytest.mark.parametrize('klass, val', TESTS.valid)
def test_from_py_valid(self, klass, val):
def test_to_py_valid(self, klass, val):
expected = QColor(val) if klass is configtypes.QtColor else val
assert klass().from_py(val) == expected
assert klass().to_py(val) == expected
@pytest.mark.parametrize('klass, val', TESTS.invalid)
def test_from_py_invalid(self, klass, val):
def test_to_py_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass().from_py(val)
klass().to_py(val)
FontDesc = collections.namedtuple('FontDesc',
@ -1187,20 +1187,20 @@ class TestFont:
return configtypes.QtFont
@pytest.mark.parametrize('val, desc', sorted(TESTS.items()))
def test_from_py_valid(self, klass, val, desc):
def test_to_py_valid(self, klass, val, desc):
if klass is configtypes.Font:
expected = val
elif klass is configtypes.QtFont:
expected = Font.fromdesc(desc)
assert klass().from_py(val) == expected
assert klass().to_py(val) == expected
def test_qtfont_float(self, qtfont_class):
"""Test QtFont's from_py with a float as point size.
"""Test QtFont's to_py with a float as point size.
We can't test the point size for equality as Qt seems to do some
rounding as appropriate.
"""
value = Font(qtfont_class().from_py('10.5pt "Foobar Neue"'))
value = Font(qtfont_class().to_py('10.5pt "Foobar Neue"'))
assert value.family() == 'Foobar Neue'
assert value.weight() == QFont.Normal
assert value.style() == QFont.StyleNormal
@ -1219,9 +1219,9 @@ class TestFont:
pytest.param('10pt', marks=font_xfail),
pytest.param('10pt ""', marks=font_xfail),
])
def test_from_py_invalid(self, klass, val):
def test_to_py_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass().from_py(val)
klass().to_py(val)
class TestFontFamily:
@ -1249,13 +1249,13 @@ class TestFontFamily:
return configtypes.FontFamily
@pytest.mark.parametrize('val', TESTS)
def test_from_py_valid(self, klass, val):
assert klass().from_py(val) == val
def test_to_py_valid(self, klass, val):
assert klass().to_py(val) == val
@pytest.mark.parametrize('val', INVALID)
def test_from_py_invalid(self, klass, val):
def test_to_py_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass().from_py(val)
klass().to_py(val)
class TestRegex:
@ -1268,22 +1268,22 @@ class TestRegex:
r'(foo|bar)?baz[fis]h',
re.compile('foobar'),
])
def test_from_py_valid(self, klass, val):
assert klass().from_py(val) == RegexEq(val)
def test_to_py_valid(self, klass, val):
assert klass().to_py(val) == RegexEq(val)
@pytest.mark.parametrize('val', [
pytest.param(r'(foo|bar))?baz[fis]h', id='unmatched parens'),
pytest.param('(' * 500, id='too many parens'),
])
def test_from_py_invalid(self, klass, val):
def test_to_py_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass().from_py(val)
klass().to_py(val)
@pytest.mark.parametrize('val', [
r'foo\Xbar',
r'foo\Cbar',
])
def test_from_py_maybe_valid(self, klass, val):
def test_to_py_maybe_valid(self, klass, val):
"""Those values are valid on some Python versions (and systems?).
On others, they raise a DeprecationWarning because of an invalid
@ -1291,7 +1291,7 @@ class TestRegex:
ValidationError.
"""
try:
klass().from_py(val)
klass().to_py(val)
except configexc.ValidationError:
pass
@ -1308,7 +1308,7 @@ class TestRegex:
m.compile.side_effect = lambda *args: warnings.warn(warning)
m.error = re.error
with pytest.raises(type(warning)):
regex.from_py('foo')
regex.to_py('foo')
def test_bad_pattern_warning(self, mocker, klass):
"""Test a simulated bad pattern warning.
@ -1322,7 +1322,7 @@ class TestRegex:
DeprecationWarning)
m.error = re.error
with pytest.raises(configexc.ValidationError):
regex.from_py('foo')
regex.to_py('foo')
@pytest.mark.parametrize('flags, expected', [
(0, 0),
@ -1340,24 +1340,19 @@ class TestRegex:
class TestDict:
# FIXME:conf how to handle {}?
@pytest.fixture
def klass(self):
return configtypes.Dict
@pytest.mark.parametrize('val', [
{"foo": "bar"},
{"foo": "bar", "baz": "fish"},
{}, # with none_ok=True
'{"foo": "bar"}',
'{"foo": "bar", "baz": "fish"}',
'{}',
])
def test_from_str_valid(self, klass, val):
expected = None if not val else val
if isinstance(val, dict):
val = json.dumps(val)
d = klass(keytype=configtypes.String(), valtype=configtypes.String(),
none_ok=True)
assert d.from_str(val) == expected
assert d.from_str(val) == json.loads(val)
@pytest.mark.parametrize('val', [
'["foo"]', # valid yaml but not a dict
@ -1387,7 +1382,7 @@ class TestDict:
if from_str:
d.from_str(json.dumps(val))
else:
d.from_py(val)
d.to_py(val)
@hypothesis.given(val=strategies.dictionaries(strategies.text(min_size=1),
strategies.booleans()))
@ -1396,7 +1391,7 @@ class TestDict:
valtype=configtypes.Bool(),
none_ok=True)
try:
converted = d.from_py(val)
converted = d.to_py(val)
assert d.from_str(d.to_str(converted)) == converted
except configexc.ValidationError:
# Invalid unicode in the string, etc...
@ -1432,59 +1427,59 @@ class TestFile:
def file_class(self):
return configtypes.File
def test_from_py_does_not_exist_file(self, os_mock):
"""Test from_py with a file which does not exist (File)."""
def test_to_py_does_not_exist_file(self, os_mock):
"""Test to_py with a file which does not exist (File)."""
os_mock.path.isfile.return_value = False
with pytest.raises(configexc.ValidationError):
configtypes.File().from_py('foobar')
configtypes.File().to_py('foobar')
def test_from_py_does_not_exist_optional_file(self, os_mock):
"""Test from_py with a file which does not exist (File)."""
def test_to_py_does_not_exist_optional_file(self, os_mock):
"""Test to_py with a file which does not exist (File)."""
os_mock.path.isfile.return_value = False
assert unrequired_class().from_py('foobar') == 'foobar'
assert unrequired_class().to_py('foobar') == 'foobar'
@pytest.mark.parametrize('val, expected', [
('/foobar', '/foobar'),
('~/foobar', '/home/foo/foobar'),
('$HOME/foobar', '/home/foo/foobar'),
])
def test_from_py_exists_abs(self, klass, os_mock, val, expected):
"""Test from_py with a file which does exist."""
def test_to_py_exists_abs(self, klass, os_mock, val, expected):
"""Test to_py with a file which does exist."""
os_mock.path.isfile.return_value = True
assert klass().from_py(val) == expected
assert klass().to_py(val) == expected
def test_from_py_exists_rel(self, klass, os_mock, monkeypatch):
"""Test from_py with a relative path to an existing file."""
def test_to_py_exists_rel(self, klass, os_mock, monkeypatch):
"""Test to_py with a relative path to an existing file."""
monkeypatch.setattr(
'qutebrowser.config.configtypes.standarddir.config',
lambda: '/home/foo/.config')
os_mock.path.isfile.return_value = True
os_mock.path.isabs.return_value = False
assert klass().from_py('foobar') == '/home/foo/.config/foobar'
assert klass().to_py('foobar') == '/home/foo/.config/foobar'
os_mock.path.join.assert_called_once_with(
'/home/foo/.config', 'foobar')
def test_from_py_expanduser(self, klass, os_mock):
"""Test if from_py expands the user correctly."""
def test_to_py_expanduser(self, klass, os_mock):
"""Test if to_py expands the user correctly."""
os_mock.path.isfile.side_effect = (lambda path:
path == '/home/foo/foobar')
os_mock.path.isabs.return_value = True
assert klass().from_py('~/foobar') == '/home/foo/foobar'
assert klass().to_py('~/foobar') == '/home/foo/foobar'
def test_from_py_expandvars(self, klass, os_mock):
"""Test if from_py expands the environment vars correctly."""
def test_to_py_expandvars(self, klass, os_mock):
"""Test if to_py expands the environment vars correctly."""
os_mock.path.isfile.side_effect = (lambda path:
path == '/home/foo/foobar')
os_mock.path.isabs.return_value = True
assert klass().from_py('$HOME/foobar') == '/home/foo/foobar'
assert klass().to_py('$HOME/foobar') == '/home/foo/foobar'
def test_from_py_invalid_encoding(self, klass, os_mock,
def test_to_py_invalid_encoding(self, klass, os_mock,
unicode_encode_err):
"""Test from_py with an invalid encoding, e.g. LC_ALL=C."""
"""Test to_py with an invalid encoding, e.g. LC_ALL=C."""
os_mock.path.isfile.side_effect = unicode_encode_err
os_mock.path.isabs.side_effect = unicode_encode_err
with pytest.raises(configexc.ValidationError):
klass().from_py('foobar')
klass().to_py('foobar')
class TestDirectory:
@ -1493,48 +1488,48 @@ class TestDirectory:
def klass(self):
return configtypes.Directory
def test_from_py_does_not_exist(self, klass, os_mock):
"""Test from_py with a directory which does not exist."""
def test_to_py_does_not_exist(self, klass, os_mock):
"""Test to_py with a directory which does not exist."""
os_mock.path.isdir.return_value = False
with pytest.raises(configexc.ValidationError):
klass().from_py('foobar')
klass().to_py('foobar')
def test_from_py_exists_abs(self, klass, os_mock):
"""Test from_py with a directory which does exist."""
def test_to_py_exists_abs(self, klass, os_mock):
"""Test to_py with a directory which does exist."""
os_mock.path.isdir.return_value = True
os_mock.path.isabs.return_value = True
assert klass().from_py('foobar') == 'foobar'
assert klass().to_py('foobar') == 'foobar'
def test_from_py_exists_not_abs(self, klass, os_mock):
"""Test from_py with a dir which does exist but is not absolute."""
def test_to_py_exists_not_abs(self, klass, os_mock):
"""Test to_py with a dir which does exist but is not absolute."""
os_mock.path.isdir.return_value = True
os_mock.path.isabs.return_value = False
with pytest.raises(configexc.ValidationError):
klass().from_py('foobar')
klass().to_py('foobar')
def test_from_py_expanduser(self, klass, os_mock):
"""Test if from_py expands the user correctly."""
def test_to_py_expanduser(self, klass, os_mock):
"""Test if to_py expands the user correctly."""
os_mock.path.isdir.side_effect = (lambda path:
path == '/home/foo/foobar')
os_mock.path.isabs.return_value = True
assert klass().from_py('~/foobar') == '/home/foo/foobar'
assert klass().to_py('~/foobar') == '/home/foo/foobar'
os_mock.path.expanduser.assert_called_once_with('~/foobar')
def test_from_py_expandvars(self, klass, os_mock, monkeypatch):
"""Test if from_py expands the user correctly."""
def test_to_py_expandvars(self, klass, os_mock, monkeypatch):
"""Test if to_py expands the user correctly."""
os_mock.path.isdir.side_effect = (lambda path:
path == '/home/foo/foobar')
os_mock.path.isabs.return_value = True
assert klass().from_py('$HOME/foobar') == '/home/foo/foobar'
assert klass().to_py('$HOME/foobar') == '/home/foo/foobar'
os_mock.path.expandvars.assert_called_once_with('$HOME/foobar')
def test_from_py_invalid_encoding(self, klass, os_mock,
def test_to_py_invalid_encoding(self, klass, os_mock,
unicode_encode_err):
"""Test from_py with an invalid encoding, e.g. LC_ALL=C."""
"""Test to_py with an invalid encoding, e.g. LC_ALL=C."""
os_mock.path.isdir.side_effect = unicode_encode_err
os_mock.path.isabs.side_effect = unicode_encode_err
with pytest.raises(configexc.ValidationError):
klass().from_py('foobar')
klass().to_py('foobar')
class TestFormatString:
@ -1547,17 +1542,17 @@ class TestFormatString:
'foo bar baz',
'{foo} {bar} baz',
])
def test_from_py_valid(self, typ, val):
assert typ.from_py(val) == val
def test_to_py_valid(self, typ, val):
assert typ.to_py(val) == val
@pytest.mark.parametrize('val', [
'{foo} {bar} {baz}',
'{foo} {bar',
'{1}',
])
def test_from_py_invalid(self, typ, val):
def test_to_py_invalid(self, typ, val):
with pytest.raises(configexc.ValidationError):
typ.from_py(val)
typ.to_py(val)
class TestShellCommand:
@ -1575,7 +1570,7 @@ class TestShellCommand:
def test_valid(self, klass, kwargs, val, expected):
cmd = klass(**kwargs)
assert cmd.from_str(val) == expected
assert cmd.from_py(expected) == expected
assert cmd.to_py(expected) == expected
@pytest.mark.parametrize('kwargs, val', [
({'placeholder': '{}'}, 'foo bar'),
@ -1606,8 +1601,8 @@ class TestProxy:
('pac+file:///tmp/proxy.pac',
pac.PACFetcher(QUrl('pac+file:///tmp/proxy.pac'))),
])
def test_from_py_valid(self, klass, val, expected):
actual = klass().from_py(val)
def test_to_py_valid(self, klass, val, expected):
actual = klass().to_py(val)
if isinstance(actual, QNetworkProxy):
actual = QNetworkProxy(actual)
assert actual == expected
@ -1617,9 +1612,9 @@ class TestProxy:
':', # invalid URL
'ftp://example.com/', # invalid scheme
])
def test_from_py_invalid(self, klass, val):
def test_to_py_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass().from_py(val)
klass().to_py(val)
def test_complete(self, klass):
"""Test complete."""
@ -1641,8 +1636,8 @@ class TestSearchEngineUrl:
'http://example.com/?q={0}',
'http://example.com/?q={0}&a={0}',
])
def test_from_py_valid(self, klass, val):
assert klass().from_py(val) == val
def test_to_py_valid(self, klass, val):
assert klass().to_py(val) == val
@pytest.mark.parametrize('val', [
'foo', # no placeholder
@ -1651,9 +1646,9 @@ class TestSearchEngineUrl:
'{1}{}', # numbered format string variable
'{{}', # invalid format syntax
])
def test_from_py_invalid(self, klass, val):
def test_to_py_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass().from_py(val)
klass().to_py(val)
class TestFuzzyUrl:
@ -1666,16 +1661,16 @@ class TestFuzzyUrl:
('http://example.com/?q={}', QUrl('http://example.com/?q={}')),
('example.com', QUrl('http://example.com')),
])
def test_from_py_valid(self, klass, val, expected):
assert klass().from_py(val) == expected
def test_to_py_valid(self, klass, val, expected):
assert klass().to_py(val) == expected
@pytest.mark.parametrize('val', [
'::foo', # invalid URL
'foo bar', # invalid search term
])
def test_from_py_invalid(self, klass, val):
def test_to_py_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass().from_py(val)
klass().to_py(val)
class TestPadding:
@ -1684,15 +1679,10 @@ class TestPadding:
def klass(self):
return configtypes.Padding
def test_from_py_valid(self, klass):
def test_to_py_valid(self, klass):
val = {'top': 1, 'bottom': 2, 'left': 3, 'right': 4}
expected = configtypes.PaddingValues(1, 2, 3, 4)
assert klass().from_py(val) == expected
def test_from_str_valid(self, klass):
val = '{"top": 1, "bottom": 2, "left": 3, "right": 4}'
expected = configtypes.PaddingValues(1, 2, 3, 4)
assert klass().from_str(val) == expected
assert klass().to_py(val) == expected
@pytest.mark.parametrize('val', [
{'top': 1, 'bottom': 2, 'left': 3, 'right': 4, 'foo': 5},
@ -1701,9 +1691,9 @@ class TestPadding:
{'top': -1, 'bottom': 2, 'left': 3, 'right': 4},
{'top': 0.1, 'bottom': 2, 'left': 3, 'right': 4},
])
def test_from_py_invalid(self, klass, val):
def test_to_py_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass().from_py(val)
klass().to_py(val)
class TestEncoding:
@ -1713,12 +1703,12 @@ class TestEncoding:
return configtypes.Encoding
@pytest.mark.parametrize('val', ['utf-8', 'UTF-8', 'iso8859-1'])
def test_from_py(self, klass, val):
assert klass().from_py(val) == val
def test_to_py(self, klass, val):
assert klass().to_py(val) == val
def test_from_py_invalid(self, klass):
def test_to_py_invalid(self, klass):
with pytest.raises(configexc.ValidationError):
klass().from_py('blubber')
klass().to_py('blubber')
class TestUrl:
@ -1733,12 +1723,12 @@ class TestUrl:
return configtypes.Url
@pytest.mark.parametrize('val, expected', list(TESTS.items()))
def test_from_py_valid(self, klass, val, expected):
assert klass().from_py(val) == expected
def test_to_py_valid(self, klass, val, expected):
assert klass().to_py(val) == expected
def test_from_py_invalid(self, klass):
def test_to_py_invalid(self, klass):
with pytest.raises(configexc.ValidationError):
klass().from_py('+')
klass().to_py('+')
class TestSessionName:
@ -1747,12 +1737,12 @@ class TestSessionName:
def klass(self):
return configtypes.SessionName
def test_from_py_valid(self, klass):
assert klass().from_py('foobar') == 'foobar'
def test_to_py_valid(self, klass):
assert klass().to_py('foobar') == 'foobar'
def test_from_py_invalid(self, klass):
def test_to_py_invalid(self, klass):
with pytest.raises(configexc.ValidationError):
klass().from_py('_foo')
klass().to_py('_foo')
class TestConfirmQuit:
@ -1768,9 +1758,9 @@ class TestConfirmQuit:
return configtypes.ConfirmQuit
@pytest.mark.parametrize('val', TESTS)
def test_from_py_valid(self, klass, val):
def test_to_py_valid(self, klass, val):
cq = klass(none_ok=True)
assert cq.from_py(val) == val
assert cq.to_py(val) == val
assert cq.from_str(json.dumps(val)) == val
@pytest.mark.parametrize('val', [
@ -1780,9 +1770,9 @@ class TestConfirmQuit:
['always', 'downloads'], # always combined
['never', 'downloads'], # never combined
])
def test_from_py_invalid(self, klass, val):
def test_to_py_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass().from_py(val)
klass().to_py(val)
def test_complete(self, klass):
"""Test completing by doing some samples."""
@ -1804,12 +1794,12 @@ class TestTimestampTemplate:
return configtypes.TimestampTemplate
@pytest.mark.parametrize('val', ['foobar', '%H:%M', 'foo %H bar %M'])
def test_from_py_valid(self, klass, val):
assert klass().from_py(val) == val
def test_to_py_valid(self, klass, val):
assert klass().to_py(val) == val
def test_from_py_invalid(self, klass):
def test_to_py_invalid(self, klass):
with pytest.raises(configexc.ValidationError):
klass().from_py('%')
klass().to_py('%')
@pytest.mark.parametrize('first, second, equal', [