Repository: aaugustin/websockets
Pull request #511 (2018-11-04 17:06:20)

Add support for bytes-like objects

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

[![Impacted file tree graph](https://codecov.io/gh/aaugustin/websockets/pull/511/graphs/tree.svg?width=650&token=vqr9oExbGY&height=150&src=pr)](https://codecov.io/gh/aaugustin/websockets/pull/511?src=pr&el=tree)

```diff
@@          Coverage Diff           @@
##           master   #511    +/-   ##
======================================
  Coverage     100%   100%            
======================================
  Files          31     31            
  Lines        3680   3782   +102     
  Branches      363    370     +7     
======================================
+ Hits         3680   3782   +102
```


| [Impacted Files](https://codecov.io/gh/aaugustin/websockets/pull/511?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/websockets/utils.py](https://codecov.io/gh/aaugustin/websockets/pull/511/diff?src=pr&el=tree#diff-c3JjL3dlYnNvY2tldHMvdXRpbHMucHk=) | `100% <ø> (ø)` | :arrow_up: |
| [tests/test\_utils.py](https://codecov.io/gh/aaugustin/websockets/pull/511/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF91dGlscy5weQ==) | `100% <100%> (ø)` | :arrow_up: |
| [tests/test\_framing.py](https://codecov.io/gh/aaugustin/websockets/pull/511/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9mcmFtaW5nLnB5) | `100% <100%> (ø)` | :arrow_up: |
| [tests/test\_protocol.py](https://codecov.io/gh/aaugustin/websockets/pull/511/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9wcm90b2NvbC5weQ==) | `100% <100%> (ø)` | :arrow_up: |
| [src/websockets/framing.py](https://codecov.io/gh/aaugustin/websockets/pull/511/diff?src=pr&el=tree#diff-c3JjL3dlYnNvY2tldHMvZnJhbWluZy5weQ==) | `100% <100%> (ø)` | :arrow_up: |
| [src/websockets/protocol.py](https://codecov.io/gh/aaugustin/websockets/pull/511/diff?src=pr&el=tree#diff-c3JjL3dlYnNvY2tldHMvcHJvdG9jb2wucHk=) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aaugustin/websockets/pull/511?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/aaugustin/websockets/pull/511?src=pr&el=footer). Last update [771a5f2...8425f56](https://codecov.io/gh/aaugustin/websockets/pull/511?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 393abf1..3ec3544 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -13,6 +13,13 @@ Changelog
     **Version 8.0 adds the reason phrase to the return type of the low-level
     API** :func:`~http.read_response` **.**
 
+Also:
+
+* :meth:`~protocol.WebSocketCommonProtocol.send`,
+  :meth:`~protocol.WebSocketCommonProtocol.ping`, and
+  :meth:`~protocol.WebSocketCommonProtocol.pong` support bytes-like types
+  :class:`bytearray` and :class:`memoryview` in addition to :class:`bytes`.
+
 7.0
 ...
 
diff --git a/src/websockets/framing.py b/src/websockets/framing.py
index 00a24d8..feebd39 100644
--- a/src/websockets/framing.py
+++ b/src/websockets/framing.py
@@ -34,6 +34,7 @@ __all__ = [
     'OP_PING',
     'OP_PONG',
     'Frame',
+    'prepare_data',
     'encode_data',
     'parse_close',
     'serialize_close',
@@ -235,20 +236,56 @@ class Frame(FrameData):
             raise WebSocketProtocolError("Invalid opcode: {}".format(frame.opcode))
 
 
+def prepare_data(data):
+    """
+    Convert a string or byte-like object to an opcode and a bytes-like object.
+
+    This function is designed for data frames.
+
+    If ``data`` is a :class:`str`, return ``OP_TEXT`` and a :class:`bytes`
+    object encoding ``data`` in UTF-8.
+
+    If ``data`` is a bytes-like object, return ``OP_BINARY`` and a bytes-like
+    object.
+
+    Raise :exc:`TypeError` for other inputs.
+
+    """
+    if isinstance(data, str):
+        return OP_TEXT, data.encode('utf-8')
+    elif isinstance(data, collections.abc.ByteString):
+        return OP_BINARY, data
+    elif isinstance(data, memoryview):
+        if data.c_contiguous:
+            return OP_BINARY, data
+        else:
+            return OP_BINARY, data.tobytes()
+    else:
+        raise TypeError("data must be bytes-like or str")
+
+
 def encode_data(data):
     """
-    Helper that converts :class:`str` or :class:`bytes` to :class:`bytes`.
+    Convert a string or byte-like object to bytes.
+
+    This function is designed for ping and pong frames.
+
+    If ``data`` is a :class:`str`, return a :class:`bytes` object encoding
+    ``data`` in UTF-8.
+
+    If ``data`` is a bytes-like object, return a :class:`bytes` object.
 
-    :class:`str` are encoded with UTF-8.
+    Raise :exc:`TypeError` for other inputs.
 
     """
-    # Expect str or bytes, return bytes.
     if isinstance(data, str):
         return data.encode('utf-8')
-    elif isinstance(data, bytes):
-        return data
+    elif isinstance(data, collections.abc.ByteString):
+        return bytes(data)
+    elif isinstance(data, memoryview):
+        return data.tobytes()
     else:
-        raise TypeError("data must be bytes or str")
+        raise TypeError("data must be bytes-like or str")
 
 
 def parse_close(data):
diff --git a/src/websockets/protocol.py b/src/websockets/protocol.py
index eb34c91..e154a62 100644
--- a/src/websockets/protocol.py
+++ b/src/websockets/protocol.py
@@ -448,10 +448,11 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
         """
         This coroutine sends a message.
 
-        It sends :class:`str` as a text frame and :class:`bytes` as a binary
-        frame.
+        It sends a string (:class:`str`) as a text frame and a bytes-like
+        object (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`)
+        as a binary frame.
 
-        It also accepts an iterable of :class:`str` or :class:`bytes`. Each
+        It also accepts an iterable of strings or bytes-like objects. Each
         item is treated as a message fragment and sent in its own frame. All
         items must be of the same type, or else :meth:`send` will raise a
         :exc:`TypeError` and the connection will be closed.
@@ -461,17 +462,21 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
         """
         yield from self.ensure_open()
 
-        # Unfragmented message (first because str and bytes are iterable).
+        # Unfragmented message -- this case must be handled first because
+        # strings and bytes-like objects are iterable.
 
-        if isinstance(data, str):
-            yield from self.write_frame(True, OP_TEXT, data.encode('utf-8'))
-
-        elif isinstance(data, bytes):
-            yield from self.write_frame(True, OP_BINARY, data)
+        try:
+            opcode, data = prepare_data(data)
+        except TypeError:
+            # Perhaps data is an iterator, see below.
+            pass
+        else:
+            yield from self.write_frame(True, opcode, data)
+            return
 
         # Fragmented message -- regular iterator.
 
-        elif isinstance(data, collections.abc.Iterable):
+        if isinstance(data, collections.abc.Iterable):
             iter_data = iter(data)
 
             # First fragment.
@@ -479,29 +484,21 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
                 data = next(iter_data)
             except StopIteration:
                 return
-            data_type = type(data)
-            if isinstance(data, str):
-                yield from self.write_frame(False, OP_TEXT, data.encode('utf-8'))
-                encode_data = True
-            elif isinstance(data, bytes):
-                yield from self.write_frame(False, OP_BINARY, data)
-                encode_data = False
-            else:
-                raise TypeError("data must be an iterable of bytes or str")
+            opcode, data = prepare_data(data)
+            yield from self.write_frame(False, opcode, data)
 
             # Other fragments.
             for data in iter_data:
-                if type(data) != data_type:
+                confirm_opcode, data = prepare_data(data)
+                if confirm_opcode != opcode:
                     # We're half-way through a fragmented message and we can't
                     # complete it. This makes the connection unusable.
                     self.fail_connection(1011)
                     raise TypeError("data contains inconsistent types")
-                if encode_data:
-                    data = data.encode('utf-8')
                 yield from self.write_frame(False, OP_CONT, data)
 
             # Final fragment.
-            yield from self.write_frame(True, OP_CONT, type(data)())
+            yield from self.write_frame(True, OP_CONT, b'')
 
         # Fragmented message -- asynchronous iterator
 
@@ -576,8 +573,8 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
             await pong_waiter   # only if you want to wait for the pong
 
         By default, the ping contains four random bytes. The content may be
-        overridden with the optional ``data`` argument which must be of type
-        :class:`str` (which will be encoded to UTF-8) or :class:`bytes`.
+        overridden with the optional ``data`` argument which must be a string
+        (which will be encoded to UTF-8) or a bytes-like object.
 
         """
         yield from self.ensure_open()
@@ -607,8 +604,8 @@ class WebSocketCommonProtocol(asyncio.StreamReaderProtocol):
         An unsolicited pong may serve as a unidirectional heartbeat.
 
         The content may be overridden with the optional ``data`` argument
-        which must be of type :class:`str` (which will be encoded to UTF-8) or
-        :class:`bytes`.
+        which must be a string (which will be encoded to UTF-8) or a
+        bytes-like object.
 
         """
         yield from self.ensure_open()
diff --git a/src/websockets/speedups.c b/src/websockets/speedups.c
index 4d76222..d1c2b37 100644
--- a/src/websockets/speedups.c
+++ b/src/websockets/speedups.c
@@ -10,16 +10,73 @@
 
 static const Py_ssize_t MASK_LEN = 4;
 
+/* Similar to PyBytes_AsStringAndSize, but accepts more types */
+
+static int
+_PyBytesLike_AsStringAndSize(PyObject *obj, char **buffer, Py_ssize_t *length)
+{
+    // This supports bytes, bytearrays, and C-contiguous memoryview objects,
+    // which are the most useful data structures for handling byte streams.
+    // websockets.framing.prepare_data() returns only values of these types.
+    // Any object implementing the buffer protocol could be supported, however
+    // that would require allocation or copying memory, which is expensive.
+    if (PyBytes_Check(obj))
+    {
+        *buffer = PyBytes_AS_STRING(obj);
+        *length = PyBytes_GET_SIZE(obj);
+    }
+    else if (PyByteArray_Check(obj))
+    {
+        *buffer = PyByteArray_AS_STRING(obj);
+        *length = PyByteArray_GET_SIZE(obj);
+    }
+    else if (PyMemoryView_Check(obj))
+    {
+        Py_buffer *mv_buf;
+        mv_buf = PyMemoryView_GET_BUFFER(obj);
+        if (PyBuffer_IsContiguous(mv_buf, 'C'))
+        {
+            *buffer = mv_buf->buf;
+            *length = mv_buf->len;
+        }
+        else
+        {
+            PyErr_Format(
+                PyExc_TypeError,
+                "expected a contiguous memoryview");
+            return -1;
+        }
+    }
+    else
+    {
+        PyErr_Format(
+            PyExc_TypeError,
+            "expected a bytes-like object, %.200s found",
+            Py_TYPE(obj)->tp_name);
+        return -1;
+    }
+
+    return 0;
+}
+
+/* C implementation of websockets.utils.apply_mask */
+
 static PyObject *
 apply_mask(PyObject *self, PyObject *args, PyObject *kwds)
 {
 
-    // Inputs are treated as immutable, which causes an extra memory copy.
+    // In order to support various bytes-like types, accept any Python object.
 
     static char *kwlist[] = {"data", "mask", NULL};
-    const char *input;
+    PyObject *input_obj;
+    PyObject *mask_obj;
+
+    // A pointer to a char * + length will be extracted from the data and mask
+    // arguments, possibly via a Py_buffer.
+
+    char *input;
     Py_ssize_t input_len;
-    const char *mask;
+    char *mask;
     Py_ssize_t mask_len;
 
     // Initialize a PyBytesObject then get a pointer to the underlying char *
@@ -27,10 +84,25 @@ apply_mask(PyObject *self, PyObject *args, PyObject *kwds)
 
     PyObject *result;
     char *output;
+
+    // Other variables.
+
     Py_ssize_t i = 0;
 
+    // Parse inputs.
+
     if (!PyArg_ParseTupleAndKeywords(
-            args, kwds, "y#y#", kwlist, &input, &input_len, &mask, &mask_len))
+            args, kwds, "OO", kwlist, &input_obj, &mask_obj))
+    {
+        return NULL;
+    }
+
+    if (_PyBytesLike_AsStringAndSize(input_obj, &input, &input_len) == -1)
+    {
+        return NULL;
+    }
+
+    if (_PyBytesLike_AsStringAndSize(mask_obj, &mask, &mask_len) == -1)
     {
         return NULL;
     }
@@ -41,6 +113,8 @@ apply_mask(PyObject *self, PyObject *args, PyObject *kwds)
         return NULL;
     }
 
+    // Create output.
+
     result = PyBytes_FromStringAndSize(NULL, input_len);
     if (result == NULL)
     {
@@ -50,6 +124,8 @@ apply_mask(PyObject *self, PyObject *args, PyObject *kwds)
     // Since we juste created result, we don't need error checks.
     output = PyBytes_AS_STRING(result);
 
+    // Perform the masking operation.
+
     // Apparently GCC cannot figure out the following optimizations by itself.
 
     // We need a new scope for MSVC 2010 (non C99 friendly)
diff --git a/src/websockets/utils.py b/src/websockets/utils.py
index b4083df..def9978 100644
--- a/src/websockets/utils.py
+++ b/src/websockets/utils.py
@@ -6,9 +6,14 @@ __all__ = ['apply_mask']
 
 def apply_mask(data, mask):
     """
-    Apply masking to websocket message.
+    Apply masking to the data of a WebSocket message.
+
+    ``data`` and ``mask`` are bytes-like objects.
+
+    Return :class:`bytes`.
 
     """
     if len(mask) != 4:
         raise ValueError("mask must contain 4 bytes")
+
     return bytes(b ^ m for b, m in zip(data, itertools.cycle(mask)))


Test patch:
diff --git a/tests/test_framing.py b/tests/test_framing.py
index 9da64f1..ab11f6b 100644
--- a/tests/test_framing.py
+++ b/tests/test_framing.py
@@ -152,13 +152,53 @@ class FramingTests(unittest.TestCase):
         with self.assertRaises(WebSocketProtocolError):
             self.decode(b'\x88\x7e\x00\x7e' + 126 * b'a')
 
+    def test_prepare_data_str(self):
+        self.assertEqual(prepare_data('café'), (OP_TEXT, b'caf\xc3\xa9'))
+
+    def test_prepare_data_bytes(self):
+        self.assertEqual(prepare_data(b'tea'), (OP_BINARY, b'tea'))
+
+    def test_prepare_data_bytearray(self):
+        self.assertEqual(
+            prepare_data(bytearray(b'tea')), (OP_BINARY, bytearray(b'tea'))
+        )
+
+    def test_prepare_data_memoryview(self):
+        self.assertEqual(
+            prepare_data(memoryview(b'tea')), (OP_BINARY, memoryview(b'tea'))
+        )
+
+    def test_prepare_data_non_contiguous_memoryview(self):
+        self.assertEqual(prepare_data(memoryview(b'tteeaa')[::2]), (OP_BINARY, b'tea'))
+
+    def test_prepare_data_list(self):
+        with self.assertRaises(TypeError):
+            prepare_data([])
+
+    def test_prepare_data_none(self):
+        with self.assertRaises(TypeError):
+            prepare_data(None)
+
     def test_encode_data_str(self):
         self.assertEqual(encode_data('café'), b'caf\xc3\xa9')
 
     def test_encode_data_bytes(self):
         self.assertEqual(encode_data(b'tea'), b'tea')
 
-    def test_encode_data_other(self):
+    def test_encode_data_bytearray(self):
+        self.assertEqual(encode_data(bytearray(b'tea')), b'tea')
+
+    def test_encode_data_memoryview(self):
+        self.assertEqual(encode_data(memoryview(b'tea')), b'tea')
+
+    def test_encode_data_non_contiguous_memoryview(self):
+        self.assertEqual(encode_data(memoryview(b'tteeaa')[::2]), b'tea')
+
+    def test_encode_data_list(self):
+        with self.assertRaises(TypeError):
+            encode_data([])
+
+    def test_encode_data_none(self):
         with self.assertRaises(TypeError):
             encode_data(None)
 
diff --git a/tests/test_protocol.py b/tests/test_protocol.py
index aee3289..a5eb251 100644
--- a/tests/test_protocol.py
+++ b/tests/test_protocol.py
@@ -535,6 +535,18 @@ class CommonTests:
         self.loop.run_until_complete(self.protocol.send(b'tea'))
         self.assertOneFrameSent(True, OP_BINARY, b'tea')
 
+    def test_send_binary_from_bytearray(self):
+        self.loop.run_until_complete(self.protocol.send(bytearray(b'tea')))
+        self.assertOneFrameSent(True, OP_BINARY, b'tea')
+
+    def test_send_binary_from_memoryview(self):
+        self.loop.run_until_complete(self.protocol.send(memoryview(b'tea')))
+        self.assertOneFrameSent(True, OP_BINARY, b'tea')
+
+    def test_send_binary_from_non_contiguous_memoryview(self):
+        self.loop.run_until_complete(self.protocol.send(memoryview(b'tteeaa')[::2]))
+        self.assertOneFrameSent(True, OP_BINARY, b'tea')
+
     def test_send_type_error(self):
         with self.assertRaises(TypeError):
             self.loop.run_until_complete(self.protocol.send(42))
@@ -554,6 +566,30 @@ class CommonTests:
             (False, OP_BINARY, b'te'), (False, OP_CONT, b'a'), (True, OP_CONT, b'')
         )
 
+    def test_send_iterable_binary_from_bytearray(self):
+        self.loop.run_until_complete(
+            self.protocol.send([bytearray(b'te'), bytearray(b'a')])
+        )
+        self.assertFramesSent(
+            (False, OP_BINARY, b'te'), (False, OP_CONT, b'a'), (True, OP_CONT, b'')
+        )
+
+    def test_send_iterable_binary_from_memoryview(self):
+        self.loop.run_until_complete(
+            self.protocol.send([memoryview(b'te'), memoryview(b'a')])
+        )
+        self.assertFramesSent(
+            (False, OP_BINARY, b'te'), (False, OP_CONT, b'a'), (True, OP_CONT, b'')
+        )
+
+    def test_send_iterable_binary_from_non_contiguous_memoryview(self):
+        self.loop.run_until_complete(
+            self.protocol.send([memoryview(b'ttee')[::2], memoryview(b'aa')[::2]])
+        )
+        self.assertFramesSent(
+            (False, OP_BINARY, b'te'), (False, OP_CONT, b'a'), (True, OP_CONT, b'')
+        )
+
     def test_send_empty_iterable(self):
         self.loop.run_until_complete(self.protocol.send([]))
         self.assertNoFrameSent()
@@ -616,6 +652,18 @@ class CommonTests:
         self.loop.run_until_complete(self.protocol.ping(b'tea'))
         self.assertOneFrameSent(True, OP_PING, b'tea')
 
+    def test_ping_binary_from_bytearray(self):
+        self.loop.run_until_complete(self.protocol.ping(bytearray(b'tea')))
+        self.assertOneFrameSent(True, OP_PING, b'tea')
+
+    def test_ping_binary_from_memoryview(self):
+        self.loop.run_until_complete(self.protocol.ping(memoryview(b'tea')))
+        self.assertOneFrameSent(True, OP_PING, b'tea')
+
+    def test_ping_binary_from_non_contiguous_memoryview(self):
+        self.loop.run_until_complete(self.protocol.ping(memoryview(b'tteeaa')[::2]))
+        self.assertOneFrameSent(True, OP_PING, b'tea')
+
     def test_ping_type_error(self):
         with self.assertRaises(TypeError):
             self.loop.run_until_complete(self.protocol.ping(42))
@@ -661,6 +709,18 @@ class CommonTests:
         self.loop.run_until_complete(self.protocol.pong(b'tea'))
         self.assertOneFrameSent(True, OP_PONG, b'tea')
 
+    def test_pong_binary_from_bytearray(self):
+        self.loop.run_until_complete(self.protocol.pong(bytearray(b'tea')))
+        self.assertOneFrameSent(True, OP_PONG, b'tea')
+
+    def test_pong_binary_from_memoryview(self):
+        self.loop.run_until_complete(self.protocol.pong(memoryview(b'tea')))
+        self.assertOneFrameSent(True, OP_PONG, b'tea')
+
+    def test_pong_binary_from_non_contiguous_memoryview(self):
+        self.loop.run_until_complete(self.protocol.pong(memoryview(b'tteeaa')[::2]))
+        self.assertOneFrameSent(True, OP_PONG, b'tea')
+
     def test_pong_type_error(self):
         with self.assertRaises(TypeError):
             self.loop.run_until_complete(self.protocol.pong(42))
diff --git a/tests/test_utils.py b/tests/test_utils.py
index c769923..1b913fe 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,3 +1,4 @@
+import itertools
 import unittest
 
 from websockets.utils import apply_mask as py_apply_mask
@@ -8,15 +9,45 @@ class UtilsTests(unittest.TestCase):
     def apply_mask(*args, **kwargs):
         return py_apply_mask(*args, **kwargs)
 
+    apply_mask_type_combos = list(itertools.product([bytes, bytearray], repeat=2))
+
+    apply_mask_test_values = [
+        (b'', b'1234', b''),
+        (b'aBcDe', b'\x00\x00\x00\x00', b'aBcDe'),
+        (b'abcdABCD', b'1234', b'PPPPpppp'),
+        (b'abcdABCD' * 10, b'1234', b'PPPPpppp' * 10),
+    ]
+
     def test_apply_mask(self):
-        for data_in, mask, data_out in [
-            (b'', b'1234', b''),
-            (b'aBcDe', b'\x00\x00\x00\x00', b'aBcDe'),
-            (b'abcdABCD', b'1234', b'PPPPpppp'),
-            (b'abcdABCD' * 10, b'1234', b'PPPPpppp' * 10),
-        ]:
-            with self.subTest(data_in=data_in, mask=mask):
-                self.assertEqual(self.apply_mask(data_in, mask), data_out)
+        for data_type, mask_type in self.apply_mask_type_combos:
+            for data_in, mask, data_out in self.apply_mask_test_values:
+                data_in, mask = data_type(data_in), mask_type(mask)
+
+                with self.subTest(data_in=data_in, mask=mask):
+                    result = self.apply_mask(data_in, mask)
+                    self.assertEqual(result, data_out)
+
+    def test_apply_mask_memoryview(self):
+        for data_type, mask_type in self.apply_mask_type_combos:
+            for data_in, mask, data_out in self.apply_mask_test_values:
+                data_in, mask = data_type(data_in), mask_type(mask)
+                data_in, mask = memoryview(data_in), memoryview(mask)
+
+                with self.subTest(data_in=data_in, mask=mask):
+                    result = self.apply_mask(data_in, mask)
+                    self.assertEqual(result, data_out)
+
+    def test_apply_mask_non_contiguous_memoryview(self):
+        for data_type, mask_type in self.apply_mask_type_combos:
+            for data_in, mask, data_out in self.apply_mask_test_values:
+                data_in, mask = data_type(data_in), mask_type(mask)
+                data_in, mask = memoryview(data_in), memoryview(mask)
+                data_in, mask = data_in[::-1], mask[::-1]
+                data_out = data_out[::-1]
+
+                with self.subTest(data_in=data_in, mask=mask):
+                    result = self.apply_mask(data_in, mask)
+                    self.assertEqual(result, data_out)
 
     def test_apply_mask_check_input_types(self):
         for data_in, mask in [(None, None), (b'abcd', None), (None, b'abcd')]:
@@ -46,3 +77,16 @@ else:
         @staticmethod
         def apply_mask(*args, **kwargs):
             return c_apply_mask(*args, **kwargs)
+
+        def test_apply_mask_non_contiguous_memoryview(self):
+            for data_type, mask_type in self.apply_mask_type_combos:
+                for data_in, mask, data_out in self.apply_mask_test_values:
+                    data_in, mask = data_type(data_in), mask_type(mask)
+                    data_in, mask = memoryview(data_in), memoryview(mask)
+                    data_in, mask = data_in[::-1], mask[::-1]
+                    data_out = data_out[::-1]
+
+                    with self.subTest(data_in=data_in, mask=mask):
+                        # The C extension only supports contiguous memoryviews.
+                        with self.assertRaises(TypeError):
+                            self.apply_mask(data_in, mask)