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

View File

@ -12,7 +12,7 @@ _TEST_UUID_STR = str(_TEST_UUID)
_TEST_UUID_STR_SANS_HYPHENS = _TEST_UUID_STR.replace('-', '') _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), ('123', None, None, None, 123),
('01', None, None, None, 1), ('01', None, None, None, 1),
('001', 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, 12, None),
('12', 2, 13, 13, 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) 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', '', ' '] + ['0x0F', 'something', '', ' '] +
['123' + w for w in string.whitespace] + ['123' + w for w in string.whitespace] +
[w + '123' 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() c = converters.IntConverter()
assert c.convert(fragment) is None assert c.convert(value) is None
@pytest.mark.parametrize('num_digits', [0, -1, -10]) @pytest.mark.parametrize('num_digits', [0, -1, -10])
@ -61,7 +61,7 @@ def test_int_converter_invalid_config(num_digits):
converters.IntConverter(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)),
('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)), ('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),
('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) c = converters.DateTimeConverter(format_string)
assert c.convert(fragment) == expected assert c.convert(value) == expected
def test_datetime_converter_default_format(): 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) 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, _TEST_UUID),
(_TEST_UUID_STR.replace('-', '', 1), _TEST_UUID), (_TEST_UUID_STR.replace('-', '', 1), _TEST_UUID),
(_TEST_UUID_STR_SANS_HYPHENS, _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[:-1] + 'g', None),
(_TEST_UUID_STR.replace('-', '_'), None), (_TEST_UUID_STR.replace('-', '_'), None),
]) ])
def test_uuid_converter(fragment, expected): def test_uuid_converter(value, expected):
c = converters.UUIDConverter() c = converters.UUIDConverter()
assert c.convert(fragment) == expected assert c.convert(value) == expected