feat(routing): Add ABC for URI template field converters (#1084)

Also rename "fragment" to "value" to be consistent with the terminology
used in the docstrings.
This commit is contained in:
Kurt Griffiths 2017-07-12 22:21:53 -06:00 committed by John Vrbanac
parent 5c2d118fe9
commit b70ae05c2d
2 changed files with 46 additions and 26 deletions

View File

@ -12,22 +12,42 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import abc
from datetime import datetime
import uuid
import six
# PERF(kgriffs): Avoid an extra namespace lookup when using this function
strptime = datetime.strptime
class IntConverter(object):
@six.add_metaclass(abc.ABCMeta)
class BaseConverter(object):
"""Abstract base class for URI template field converters."""
@abc.abstractmethod # pragma: no cover
def convert(self, value):
"""Convert a URI template field value to another format or type.
Args:
value (str): Original string to convert.
Returns:
object: Converted field value, or ``None`` if the field
can not be converted.
"""
class IntConverter(BaseConverter):
"""Converts a field value to an int.
Keyword Args:
num_digits (int): Require the value to have the given
number of digits.
min (int): Reject the value if it is less than this value.
max (int): Reject the value if it is greater than this value.
min (int): Reject the value if it is less than this number.
max (int): Reject the value if it is greater than this number.
"""
__slots__ = ('_num_digits', '_min', '_max')
@ -40,20 +60,20 @@ class IntConverter(object):
self._min = min
self._max = max
def convert(self, fragment):
if self._num_digits is not None and len(fragment) != self._num_digits:
def convert(self, value):
if self._num_digits is not None and len(value) != self._num_digits:
return None
# NOTE(kgriffs): int() will accept numbers with preceding or
# trailing whitespace, so we need to do our own check. Using
# strip() is faster than either a regex or a series of or'd
# membership checks via "in", esp. as the length of contiguous
# numbers in the fragment grows.
if fragment.strip() != fragment:
# numbers in the value grows.
if value.strip() != value:
return None
try:
value = int(fragment)
value = int(value)
except ValueError:
return None
@ -66,7 +86,7 @@ class IntConverter(object):
return value
class DateTimeConverter(object):
class DateTimeConverter(BaseConverter):
"""Converts a field value to a datetime.
Keyword Args:
@ -80,14 +100,14 @@ class DateTimeConverter(object):
def __init__(self, format_string='%Y-%m-%dT%H:%M:%SZ'):
self._format_string = format_string
def convert(self, fragment):
def convert(self, value):
try:
return strptime(fragment, self._format_string)
return strptime(value, self._format_string)
except ValueError:
return None
class UUIDConverter(object):
class UUIDConverter(BaseConverter):
"""Converts a field value to a uuid.UUID.
In order to be converted, the field value must consist of a
@ -95,9 +115,9 @@ class UUIDConverter(object):
Note, however, that hyphens and the URN prefix are optional.
"""
def convert(self, fragment):
def convert(self, value):
try:
return uuid.UUID(fragment)
return uuid.UUID(value)
except ValueError:
return None

View File

@ -12,7 +12,7 @@ _TEST_UUID_STR = str(_TEST_UUID)
_TEST_UUID_STR_SANS_HYPHENS = _TEST_UUID_STR.replace('-', '')
@pytest.mark.parametrize('fragment, num_digits, min, max, expected', [
@pytest.mark.parametrize('value, num_digits, min, max, expected', [
('123', None, None, None, 123),
('01', None, None, None, 1),
('001', None, None, None, 1),
@ -40,19 +40,19 @@ _TEST_UUID_STR_SANS_HYPHENS = _TEST_UUID_STR.replace('-', '')
('12', 2, 13, 12, None),
('12', 2, 13, 13, None),
])
def test_int_converter(fragment, num_digits, min, max, expected):
def test_int_converter(value, num_digits, min, max, expected):
c = converters.IntConverter(num_digits, min, max)
assert c.convert(fragment) == expected
assert c.convert(value) == expected
@pytest.mark.parametrize('fragment', (
@pytest.mark.parametrize('value', (
['0x0F', 'something', '', ' '] +
['123' + w for w in string.whitespace] +
[w + '123' for w in string.whitespace]
))
def test_int_converter_malformed(fragment):
def test_int_converter_malformed(value):
c = converters.IntConverter()
assert c.convert(fragment) is None
assert c.convert(value) is None
@pytest.mark.parametrize('num_digits', [0, -1, -10])
@ -61,7 +61,7 @@ def test_int_converter_invalid_config(num_digits):
converters.IntConverter(num_digits)
@pytest.mark.parametrize('fragment, format_string, expected', [
@pytest.mark.parametrize('value, format_string, expected', [
('07-03-17', '%m-%d-%y', datetime(2017, 7, 3)),
('07-03-17 ', '%m-%d-%y ', datetime(2017, 7, 3)),
('2017-07-03T14:30:01Z', '%Y-%m-%dT%H:%M:%SZ', datetime(2017, 7, 3, 14, 30, 1)),
@ -73,9 +73,9 @@ def test_int_converter_invalid_config(num_digits):
(' 07-03-17', '%m-%d-%y', None),
('07 -03-17', '%m-%d-%y', None),
])
def test_datetime_converter(fragment, format_string, expected):
def test_datetime_converter(value, format_string, expected):
c = converters.DateTimeConverter(format_string)
assert c.convert(fragment) == expected
assert c.convert(value) == expected
def test_datetime_converter_default_format():
@ -83,7 +83,7 @@ def test_datetime_converter_default_format():
assert c.convert('2017-07-03T14:30:01Z') == datetime(2017, 7, 3, 14, 30, 1)
@pytest.mark.parametrize('fragment, expected', [
@pytest.mark.parametrize('value, expected', [
(_TEST_UUID_STR, _TEST_UUID),
(_TEST_UUID_STR.replace('-', '', 1), _TEST_UUID),
(_TEST_UUID_STR_SANS_HYPHENS, _TEST_UUID),
@ -98,6 +98,6 @@ def test_datetime_converter_default_format():
(_TEST_UUID_STR[:-1] + 'g', None),
(_TEST_UUID_STR.replace('-', '_'), None),
])
def test_uuid_converter(fragment, expected):
def test_uuid_converter(value, expected):
c = converters.UUIDConverter()
assert c.convert(fragment) == expected
assert c.convert(value) == expected