Repository: falconry/falcon
Pull request #1084 (2017-07-12 17:10:13)

feat(routing): Add ABC for URI template field converters
Also rename "fragment" to "value" to be consistent with the terminology used in the docstrings.

TODO: In a followup PR, reference the converter classes in the docs.

codecov[bot]: # [Codecov](https://codecov.io/gh/falconry/falcon/pull/1084?src=pr&el=h1) Report
> Merging [#1084](https://codecov.io/gh/falconry/falcon/pull/1084?src=pr&el=desc) into [master](https://codecov.io/gh/falconry/falcon/commit/5c2d118fe93979bb3e679d3f24107d6cef8411c6?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/falconry/falcon/pull/1084/graphs/tree.svg?width=650&height=150&src=pr&token=zOtkfNQ3h6)](https://codecov.io/gh/falconry/falcon/pull/1084?src=pr&el=tree)

```diff
@@          Coverage Diff           @@
##           master   #1084   +/-   ##
======================================
  Coverage     100%    100%           
======================================
  Files          38      38           
  Lines        2350    2354    +4     
  Branches      331     332    +1     
======================================
+ Hits         2350    2354    +4
```


| [Impacted Files](https://codecov.io/gh/falconry/falcon/pull/1084?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [falcon/routing/converters.py](https://codecov.io/gh/falconry/falcon/pull/1084?src=pr&el=tree#diff-ZmFsY29uL3JvdXRpbmcvY29udmVydGVycy5weQ==) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/falconry/falcon/pull/1084?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/falconry/falcon/pull/1084?src=pr&el=footer). Last update [5c2d118...dee4296](https://codecov.io/gh/falconry/falcon/pull/1084?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
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
 


Test patch:
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