Initial implementation of to_str for configtypes

This commit is contained in:
Florian Bruhin 2017-06-15 17:50:12 +02:00
parent 0c1f480fc1
commit 8ea3d92697
2 changed files with 161 additions and 32 deletions

View File

@ -29,6 +29,7 @@ import warnings
import datetime import datetime
import functools import functools
import operator import operator
import json
import yaml import yaml
from PyQt5.QtCore import QUrl, Qt from PyQt5.QtCore import QUrl, Qt
@ -199,19 +200,15 @@ class BaseType:
""" """
raise NotImplementedError raise NotImplementedError
# def to_str(self, value): def to_str(self, value):
# """Get a string from the setting value. """Get a string from the setting value.
# The resulting string should be parseable again by from_str. The resulting string should be parseable again by from_str.
# """ """
# raise NotImplementedError if value is None:
return ''
# def to_py(self, value): assert isinstance(value, str)
# """Get a Python/YAML value from the setting value. return value
# The resulting value should be parseable again by from_py.
# """
# raise NotImplementedError
def complete(self): def complete(self):
"""Return a list of possible values for completion. """Return a list of possible values for completion.
@ -258,11 +255,6 @@ class MappingType(BaseType):
self._validate_valid_values(value.lower()) self._validate_valid_values(value.lower())
return self.MAPPING[value.lower()] return self.MAPPING[value.lower()]
# def to_str(self, value):
# reverse_mapping = {v: k for k, v in self.MAPPING.items()}
# assert len(self.MAPPING) == len(reverse_mapping)
# return reverse_mapping[value]
class String(BaseType): class String(BaseType):
@ -401,6 +393,11 @@ class List(BaseType):
"be set!".format(self.length)) "be set!".format(self.length))
return [self.valtype.from_py(v) for v in value] return [self.valtype.from_py(v) for v in value]
def to_str(self, value):
if value is None:
return ''
return json.dumps(value)
class FlagList(List): class FlagList(List):
@ -472,6 +469,14 @@ class Bool(BaseType):
except KeyError: except KeyError:
raise configexc.ValidationError(value, "must be a boolean!") raise configexc.ValidationError(value, "must be a boolean!")
def to_str(self, value):
mapping = {
None: '',
True: 'true',
False: 'false',
}
return mapping[value]
class BoolAsk(Bool): class BoolAsk(Bool):
@ -495,6 +500,15 @@ class BoolAsk(Bool):
return 'ask' return 'ask'
return super().from_str(value) return super().from_str(value)
def to_str(self, value):
mapping = {
None: '',
True: 'true',
False: 'false',
'ask': 'ask',
}
return mapping[value]
class _Numeric(BaseType): # pylint: disable=abstract-method class _Numeric(BaseType): # pylint: disable=abstract-method
@ -536,6 +550,11 @@ class _Numeric(BaseType): # pylint: disable=abstract-method
raise configexc.ValidationError( raise configexc.ValidationError(
value, "must be {}{} or smaller!".format(self.maxval, suffix)) value, "must be {}{} or smaller!".format(self.maxval, suffix))
def to_str(self, value):
if value is None:
return ''
return str(value)
class Int(_Numeric): class Int(_Numeric):
@ -597,6 +616,11 @@ class Perc(_Numeric):
self._validate_bounds(floatval, suffix='%') self._validate_bounds(floatval, suffix='%')
return floatval return floatval
def to_str(self, value):
if value is None:
return ''
return value
class PercOrInt(_Numeric): class PercOrInt(_Numeric):
@ -637,8 +661,8 @@ class PercOrInt(_Numeric):
def from_py(self, value): def from_py(self, value):
"""Expect a value like '42%' as string, or 23 as int.""" """Expect a value like '42%' as string, or 23 as int."""
self._basic_py_validation(value, (int, str)) self._basic_py_validation(value, (int, str))
if not value: if value is None:
return return None
if isinstance(value, str): if isinstance(value, str):
if not value.endswith('%'): if not value.endswith('%'):
@ -913,6 +937,14 @@ class Regex(BaseType):
# FIXME:conf is it okay if we ignore flags here? # FIXME:conf is it okay if we ignore flags here?
return value return value
def to_str(self, value):
if value is None:
return ''
elif isinstance(value, self._regex_type):
return value.pattern
else:
return value
class Dict(BaseType): class Dict(BaseType):
@ -954,6 +986,11 @@ class Dict(BaseType):
return {self.keytype.from_py(key): self.valtype.from_py(val) return {self.keytype.from_py(key): self.valtype.from_py(val)
for key, val in value.items()} for key, val in value.items()}
def to_str(self, value):
if value is None:
return ''
return json.dumps(value)
class File(BaseType): class File(BaseType):

View File

@ -22,6 +22,7 @@ import re
import os import os
import sys import sys
import json import json
import math
import collections import collections
import itertools import itertools
import warnings import warnings
@ -213,10 +214,31 @@ class TestAll:
@hypothesis.given(strategies.text()) @hypothesis.given(strategies.text())
@hypothesis.example('\x00') @hypothesis.example('\x00')
def test_from_str_hypothesis(self, klass, s): def test_from_str_hypothesis(self, klass, s):
typ = klass()
try: try:
klass().from_str(s) val = typ.from_str(s)
except configexc.ValidationError: except configexc.ValidationError:
pass return
# For some types, we don't actually get the internal (YAML-like) value
# back from from_str(), so we can't convert it back.
if klass in [configtypes.FuzzyUrl, configtypes.QtFont,
configtypes.ShellCommand, configtypes.Url]:
return
converted = typ.to_str(val)
# For those we only check that to_str doesn't crash, but we can't be
# sure we get the 100% same value back.
if klass in [
configtypes.Bool, # on -> true
configtypes.BoolAsk, # ditto
configtypes.Float, # 1.0 -> 1
configtypes.Int, # 00 -> 0
configtypes.PercOrInt, # ditto
]:
return
assert converted == s
def test_none_ok_true(self, klass): def test_none_ok_true(self, klass):
"""Test None and empty string values with none_ok=True.""" """Test None and empty string values with none_ok=True."""
@ -235,6 +257,9 @@ class TestAll:
with pytest.raises(configexc.ValidationError): with pytest.raises(configexc.ValidationError):
meth(value) meth(value)
def test_to_str_none(self, klass):
assert klass().to_str(None) == ''
def test_invalid_python_type(self, klass): def test_invalid_python_type(self, klass):
"""Make sure every type fails when passing an invalid Python type.""" """Make sure every type fails when passing an invalid Python type."""
with pytest.raises(configexc.ValidationError): with pytest.raises(configexc.ValidationError):
@ -349,6 +374,9 @@ class TestMappingType:
with pytest.raises(configexc.ValidationError): with pytest.raises(configexc.ValidationError):
klass().from_py(val) klass().from_py(val)
def test_to_str(self, klass):
assert klass().to_str('one') == 'one'
@pytest.mark.parametrize('typ', [configtypes.ColorSystem(), @pytest.mark.parametrize('typ', [configtypes.ColorSystem(),
configtypes.Position(), configtypes.Position(),
configtypes.SelectOnRemove()]) configtypes.SelectOnRemove()])
@ -523,20 +551,29 @@ class TestList:
expected = configtypes.ValidValues('foo', 'bar', 'baz') expected = configtypes.ValidValues('foo', 'bar', 'baz')
assert klass().get_valid_values() == expected assert klass().get_valid_values() == expected
def test_to_str(self, klass):
assert klass().to_str(["a", True] == '["a", true]')
@hypothesis.given(val=strategies.lists(strategies.just('foo'))) @hypothesis.given(val=strategies.lists(strategies.just('foo')))
def test_hypothesis(self, klass, val): def test_hypothesis(self, klass, val):
typ = klass(none_ok_outer=True)
try: try:
klass().from_py(val) converted = typ.from_py(val)
except configexc.ValidationError: except configexc.ValidationError:
pass pass
else:
assert typ.from_str(typ.to_str(converted)) == converted
@hypothesis.given(val=strategies.lists(strategies.just('foo'))) @hypothesis.given(val=strategies.lists(strategies.just('foo')))
def test_hypothesis_text(self, klass, val): def test_hypothesis_text(self, klass, val):
typ = klass()
text = json.dumps(val) text = json.dumps(val)
try: try:
klass().from_str(text) converted = typ.from_str(text)
except configexc.ValidationError: except configexc.ValidationError:
pass pass
else:
assert typ.to_str(converted) == text
class TestFlagList: class TestFlagList:
@ -622,6 +659,13 @@ class TestBool:
with pytest.raises(configexc.ValidationError): with pytest.raises(configexc.ValidationError):
klass().from_py(42) klass().from_py(42)
@pytest.mark.parametrize('val, expected', [
(True, 'true'),
(False, 'false'),
])
def test_to_str(self, klass, val, expected):
assert klass().to_str(val) == expected
class TestBoolAsk: class TestBoolAsk:
@ -654,6 +698,14 @@ class TestBoolAsk:
with pytest.raises(configexc.ValidationError): with pytest.raises(configexc.ValidationError):
klass().from_py(42) klass().from_py(42)
@pytest.mark.parametrize('val, expected', [
(True, 'true'),
(False, 'false'),
('ask', 'ask'),
])
def test_to_str(self, klass, val, expected):
assert klass().to_str(val) == expected
class TestNumeric: class TestNumeric:
@ -744,12 +796,16 @@ class TestInt:
@hypothesis.given(val=strategies.integers()) @hypothesis.given(val=strategies.integers())
def test_hypothesis(self, klass, val): def test_hypothesis(self, klass, val):
klass().from_py(val) typ = klass()
converted = typ.from_py(val)
assert typ.from_str(typ.to_str(converted)) == converted
@hypothesis.given(val=strategies.integers()) @hypothesis.given(val=strategies.integers())
def test_hypothesis_text(self, klass, val): def test_hypothesis_text(self, klass, val):
text = json.dumps(val) text = json.dumps(val)
klass().from_str(text) typ = klass()
converted = typ.from_str(text)
assert typ.to_str(converted) == text
class TestFloat: class TestFloat:
@ -794,13 +850,21 @@ class TestFloat:
@hypothesis.given(val=strategies.one_of(strategies.floats(), @hypothesis.given(val=strategies.one_of(strategies.floats(),
strategies.integers())) strategies.integers()))
def test_hypothesis(self, klass, val): def test_hypothesis(self, klass, val):
klass().from_py(val) typ = klass()
converted = typ.from_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
@hypothesis.given(val=strategies.one_of(strategies.floats(), @hypothesis.given(val=strategies.one_of(strategies.floats(),
strategies.integers())) strategies.integers()))
def test_hypothesis_text(self, klass, val): def test_hypothesis_text(self, klass, val):
text = json.dumps(val) text = json.dumps(val)
klass().from_str(text) klass().from_str(text)
# Can't check for string equality with to_str here, as we can get 1.0
# for 1.
class TestPerc: class TestPerc:
@ -847,6 +911,9 @@ class TestPerc:
with pytest.raises(configexc.ValidationError): with pytest.raises(configexc.ValidationError):
klass(**kwargs).from_py(val) klass(**kwargs).from_py(val)
def test_to_str(self, klass):
assert klass().to_str('42%') == '42%'
class TestPercOrInt: class TestPercOrInt:
@ -909,13 +976,30 @@ class TestPercOrInt:
with pytest.raises(configexc.ValidationError): with pytest.raises(configexc.ValidationError):
klass().from_py(val) klass().from_py(val)
@hypothesis.given(val=strategies.one_of(strategies.integers(), @hypothesis.given(val=strategies.one_of(
strategies.integers(),
strategies.integers().map(lambda n: str(n) + '%'),
strategies.text())) strategies.text()))
def test_hypothesis(self, klass, val): def test_hypothesis(self, klass, val):
typ = klass(none_ok=True)
try: try:
klass().from_py(val) converted = typ.from_py(val)
except configexc.ValidationError: except configexc.ValidationError:
pass pass
else:
assert typ.from_str(typ.to_str(converted)) == converted
@hypothesis.given(text=strategies.one_of(
strategies.integers().map(str),
strategies.integers().map(lambda n: str(n) + '%')))
def test_hypothesis_text(self, klass, text):
typ = klass()
try:
converted = typ.from_str(text)
except configexc.ValidationError:
pass
else:
assert typ.to_str(converted) == text
class TestCommand: class TestCommand:
@ -1248,6 +1332,10 @@ class TestRegex:
typ = klass(flags=flags) typ = klass(flags=flags)
assert typ.flags == expected assert typ.flags == expected
@pytest.mark.parametrize('value', [r'foobar', re.compile(r'foobar')])
def test_to_str(self, klass, value):
assert klass().to_str(value) == 'foobar'
class TestDict: class TestDict:
@ -1304,8 +1392,10 @@ class TestDict:
strategies.booleans())) strategies.booleans()))
def test_hypothesis(self, klass, val): def test_hypothesis(self, klass, val):
d = klass(keytype=configtypes.Bool(), d = klass(keytype=configtypes.Bool(),
valtype=configtypes.Bool()) valtype=configtypes.Bool(),
d.from_py(val) none_ok=True)
converted = d.from_py(val)
assert d.from_str(d.to_str(converted)) == converted
@hypothesis.given(val=strategies.dictionaries(strategies.booleans(), @hypothesis.given(val=strategies.dictionaries(strategies.booleans(),
strategies.booleans())) strategies.booleans()))
@ -1313,9 +1403,11 @@ class TestDict:
text = json.dumps(val) text = json.dumps(val)
d = klass(keytype=configtypes.Bool(), valtype=configtypes.Bool()) d = klass(keytype=configtypes.Bool(), valtype=configtypes.Bool())
try: try:
d.from_str(text) converted = d.from_str(text)
except configexc.ValidationError: except configexc.ValidationError:
pass pass
else:
assert d.to_str(converted) == text
def unrequired_class(**kwargs): def unrequired_class(**kwargs):