diff --git a/falcon/routing/converters.py b/falcon/routing/converters.py index 16e3b21..ca71714 100644 --- a/falcon/routing/converters.py +++ b/falcon/routing/converters.py @@ -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 diff --git a/tests/test_uri_converters.py b/tests/test_uri_converters.py index a4e25ce..4954604 100644 --- a/tests/test_uri_converters.py +++ b/tests/test_uri_converters.py @@ -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