Repository: Deepwalker/trafaret

Issue:
No FromString/ToBytes?
There is a FromBytes object to convert to string. It would be nice to have the inverse to convert to bytes.

Discussion:

Patch:
diff --git a/trafaret/__init__.py b/trafaret/__init__.py
index 19f68e9..13a10af 100644
--- a/trafaret/__init__.py
+++ b/trafaret/__init__.py
@@ -23,6 +23,7 @@ from .base import (
     String,
     AnyString,
     Bytes,
+    ToBytes,
     FromBytes,
     Callable,
     Bool,
@@ -84,6 +85,7 @@ __all__ = (
     "ToDateTime",
     "AnyString",
     "Bytes",
+    "ToBytes",
     "FromBytes",
     "Float",
     "ToFloat",
diff --git a/trafaret/base.py b/trafaret/base.py
index b6f7e53..65496e5 100644
--- a/trafaret/base.py
+++ b/trafaret/base.py
@@ -684,6 +684,34 @@ class Bytes(String):
     TYPE_ERROR_CODE = codes.IS_NOT_A_BYTES_STRING
 
 
+class ToBytes(Trafaret):
+    """Get str and try to encode it with given encoding, utf-8 by default."""
+    def __init__(self, encoding='utf-8'):
+        self.encoding = encoding
+
+    def check_and_return(self, value):
+        if isinstance(value, BYTES_TYPE):
+            return value
+        elif isinstance(value, STR_TYPE):
+            try:
+                return value.encode(self.encoding)
+            except UnicodeError:
+                raise self._failure(
+                    'value cannot be encoded with %s encoding' % self.encoding,
+                    value=value,
+                    code=codes.CANNOT_BE_ENCODED,
+                )
+        else:
+            self._failure(
+                'value is not str/bytes type',
+                value=value,
+                code=codes.IS_NOT_A_STRING,
+            )
+
+    def __repr__(self):
+        return '<ToBytes>'
+
+
 class AnyString(String):
     str_type = (BYTES_TYPE, STR_TYPE)
 
diff --git a/trafaret/codes.py b/trafaret/codes.py
index afb0f68..cc71a96 100644
--- a/trafaret/codes.py
+++ b/trafaret/codes.py
@@ -29,6 +29,7 @@ IS_NOT_CALLABLE = 'is_not_callable'
 
 IS_NOT_BYTES = 'is_not_bytes'
 CANNOT_BE_DECODED = 'cannot_be_decoded'
+CANNOT_BE_ENCODED = 'cannot_be_encoded'
 
 IS_NOT_A_LIST = 'is_not_a_list'
 TOO_SHORT = 'too_short'


Test patch:
diff --git a/tests/test_base.py b/tests/test_base.py
index bff4193..8ca7cae 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -706,6 +706,24 @@ class TestDateTimeTrafaret:
         assert repr(to_datetime_repr) == '<ToDateTime %Y-%m-%d %H:%M>'
 
 
+class TestToBytesTrafaret:
+    def test_bytes(self):
+        res = t.ToBytes().check("foo")
+        assert res == b'foo'
+        res = t.ToBytes()("")
+        assert res == b''
+        res = t.ToBytes().check(b"foo")
+        assert res == b'foo'
+        res = extract_error(t.ToBytes(), 1)
+        assert res == 'value is not str/bytes type'
+        res = extract_error(t.ToBytes('ascii'), '£')
+        assert res == 'value cannot be encoded with ascii encoding'
+
+    def test_repr(self):
+        res = t.ToBytes()
+        assert repr(res) == '<ToBytes>'
+
+
 class TestFromBytesTrafaret:
     def test_bytes(self):
         res = t.FromBytes().check(b"foo")