From d74ed5a29e78fd0c1e425bbd7345fa0d7fecb35a Mon Sep 17 00:00:00 2001 From: Ethan Sarp <11684270+esarp@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:02:31 -0600 Subject: [PATCH] [mypyc] Implement bytes.endswith --- mypyc/lib-rt/CPy.h | 2 +- mypyc/lib-rt/bytes_ops.c | 38 ++++++++++++++++++++++++++++++ mypyc/primitives/bytes_ops.py | 10 ++++++++ mypyc/test-data/fixtures/ir.py | 2 ++ mypyc/test-data/irbuild-bytes.test | 13 ++++++++++ mypyc/test-data/run-bytes.test | 21 +++++++++++++++++ 6 files changed, 85 insertions(+), 1 deletion(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index df251da13fe5..488772d883c9 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -784,7 +784,7 @@ PyObject *CPyBytes_Join(PyObject *sep, PyObject *iter); CPyTagged CPyBytes_Ord(PyObject *obj); PyObject *CPyBytes_Multiply(PyObject *bytes, CPyTagged count); int CPyBytes_Startswith(PyObject *self, PyObject *subobj); - +int CPyBytes_Endswith(PyObject *self, PyObject *subobj); int CPyBytes_Compare(PyObject *left, PyObject *right); diff --git a/mypyc/lib-rt/bytes_ops.c b/mypyc/lib-rt/bytes_ops.c index 25718b4603b3..e4122013cb0f 100644 --- a/mypyc/lib-rt/bytes_ops.c +++ b/mypyc/lib-rt/bytes_ops.c @@ -209,3 +209,41 @@ int CPyBytes_Startswith(PyObject *self, PyObject *subobj) { } return ret; } + +int CPyBytes_Endswith(PyObject *self, PyObject *subobj) { + if (PyBytes_CheckExact(self) && PyBytes_CheckExact(subobj)) { + if (self == subobj) { + return 1; + } + + Py_ssize_t subobj_len = PyBytes_GET_SIZE(subobj); + if (subobj_len == 0) { + return 1; + } + + Py_ssize_t self_len = PyBytes_GET_SIZE(self); + if (subobj_len > self_len) { + return 0; + } + + const char *self_buf = PyBytes_AS_STRING(self); + const char *subobj_buf = PyBytes_AS_STRING(subobj); + + return memcmp(self_buf + (self_len - subobj_len), subobj_buf, (size_t)subobj_len) == 0 ? 1 : 0; + } + _Py_IDENTIFIER(endswith); + PyObject *name = _PyUnicode_FromId(&PyId_endswith); + if (name == NULL) { + return 2; + } + PyObject *result = PyObject_CallMethodOneArg(self, name, subobj); + if (result == NULL) { + return 2; + } + int ret = PyObject_IsTrue(result); + Py_DECREF(result); + if (ret < 0) { + return 2; + } + return ret; +} diff --git a/mypyc/primitives/bytes_ops.py b/mypyc/primitives/bytes_ops.py index 728da4181135..05880bedad39 100644 --- a/mypyc/primitives/bytes_ops.py +++ b/mypyc/primitives/bytes_ops.py @@ -150,6 +150,16 @@ error_kind=ERR_MAGIC, ) +# bytes.endswith(bytes) +method_op( + name="endswith", + arg_types=[bytes_rprimitive, bytes_rprimitive], + return_type=c_int_rprimitive, + c_function_name="CPyBytes_Endswith", + truncated_type=bool_rprimitive, + error_kind=ERR_MAGIC, +) + # Join bytes objects and return a new bytes. # The first argument is the total number of the following bytes. bytes_build_op = custom_op( diff --git a/mypyc/test-data/fixtures/ir.py b/mypyc/test-data/fixtures/ir.py index 592f6676e95e..8aaadc238fe6 100644 --- a/mypyc/test-data/fixtures/ir.py +++ b/mypyc/test-data/fixtures/ir.py @@ -180,6 +180,7 @@ def join(self, x: Iterable[object]) -> bytes: ... def decode(self, encoding: str=..., errors: str=...) -> str: ... def translate(self, t: bytes) -> bytes: ... def startswith(self, t: bytes) -> bool: ... + def endswith(self, t: bytes) -> bool: ... def __iter__(self) -> Iterator[int]: ... class bytearray: @@ -194,6 +195,7 @@ def __setitem__(self, i: int, o: int) -> None: ... def __getitem__(self, i: int) -> int: ... def decode(self, x: str = ..., y: str = ...) -> str: ... def startswith(self, t: bytes) -> bool: ... + def endswith(self, t: bytes) -> bool: ... class bool(int): def __init__(self, o: object = ...) -> None: ... diff --git a/mypyc/test-data/irbuild-bytes.test b/mypyc/test-data/irbuild-bytes.test index 5e7c546eb25a..e454994d4492 100644 --- a/mypyc/test-data/irbuild-bytes.test +++ b/mypyc/test-data/irbuild-bytes.test @@ -261,3 +261,16 @@ L0: r0 = CPyBytes_Startswith(a, b) r1 = truncate r0: i32 to builtins.bool return r1 + +[case testBytesEndsWith] +def f(a: bytes, b: bytes) -> bool: + return a.endswith(b) +[out] +def f(a, b): + a, b :: bytes + r0 :: i32 + r1 :: bool +L0: + r0 = CPyBytes_Endswith(a, b) + r1 = truncate r0: i32 to builtins.bool + return r1 diff --git a/mypyc/test-data/run-bytes.test b/mypyc/test-data/run-bytes.test index 6e4b57152a4b..35679685f4cb 100644 --- a/mypyc/test-data/run-bytes.test +++ b/mypyc/test-data/run-bytes.test @@ -221,6 +221,27 @@ def test_startswith() -> None: assert test.startswith(b'some') assert not test.startswith(b'other') +def test_endswith() -> None: + # Test default behavior + test = b'some string' + assert test.endswith(b'string') + assert test.endswith(b'some string') + assert not test.endswith(b'other') + assert not test.endswith(b'some string but longer') + + # Test empty cases + assert test.endswith(b'') + assert b''.endswith(b'') + assert not b''.endswith(test) + + # Test bytearray to verify slow paths + assert test.endswith(bytearray(b'string')) + assert not test.endswith(bytearray(b'other')) + + test = bytearray(b'some string') + assert test.endswith(b'string') + assert not test.endswith(b'other') + [case testBytesSlicing] def test_bytes_slicing() -> None: b = b'abcdefg'