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:
parent
5c2d118fe9
commit
b70ae05c2d
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue