From 4e85229f641a2ebe65c7d07af84e76ac6b0b8adc 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 1/3] [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 68c0be2bd748..af5247e64393 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -790,7 +790,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 2a03e32a013a..9552e54ff5ce 100644 --- a/mypyc/lib-rt/bytes_ops.c +++ b/mypyc/lib-rt/bytes_ops.c @@ -183,3 +183,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 0b32c7937ba1..5775bcda6469 100644 --- a/mypyc/primitives/bytes_ops.py +++ b/mypyc/primitives/bytes_ops.py @@ -133,6 +133,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 c608a68c26e9..c21eeca9d2fe 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 | bytearray) -> bytes: ... def startswith(self, t: bytes | bytearray) -> bool: ... + def endswith(self, t: bytes | bytearray) -> bool: ... def __iter__(self) -> Iterator[int]: ... class bytearray: @@ -197,6 +198,7 @@ def __getitem__(self, i: int) -> int: ... def __getitem__(self, i: slice) -> bytearray: ... 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 391be56f00d2..c03bcf5d2067 100644 --- a/mypyc/test-data/irbuild-bytes.test +++ b/mypyc/test-data/irbuild-bytes.test @@ -266,6 +266,19 @@ L0: 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 + [case testBytesVsBytearray] def bytes_func(b: bytes) -> None: pass def bytearray_func(ba: bytearray) -> None: pass diff --git a/mypyc/test-data/run-bytes.test b/mypyc/test-data/run-bytes.test index 89ccfd66d288..afb863a3b350 100644 --- a/mypyc/test-data/run-bytes.test +++ b/mypyc/test-data/run-bytes.test @@ -221,6 +221,27 @@ def test_startswith() -> None: assert test2.startswith(b'some') assert not test2.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' From c4f5081bf202977e37e9717f3d9e77ff759d5ee7 Mon Sep 17 00:00:00 2001 From: Ethan Sarp <11684270+esarp@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:24:05 -0600 Subject: [PATCH 2/3] Remove PY_IDENTIFIER --- mypyc/lib-rt/bytes_ops.c | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/mypyc/lib-rt/bytes_ops.c b/mypyc/lib-rt/bytes_ops.c index 9552e54ff5ce..ba5b64df94b3 100644 --- a/mypyc/lib-rt/bytes_ops.c +++ b/mypyc/lib-rt/bytes_ops.c @@ -205,12 +205,7 @@ int CPyBytes_Endswith(PyObject *self, PyObject *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); + PyObject *result = PyObject_CallMethodOneArg(self, mypyc_interned_str.endswith, subobj); if (result == NULL) { return 2; } From 4c55a577587a5e6b9effd7cdd0e0685855d3f9ce Mon Sep 17 00:00:00 2001 From: Ethan Sarp <11684270+esarp@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:39:04 -0600 Subject: [PATCH 3/3] Add static data for endswith; fix test typing --- mypyc/lib-rt/static_data.c | 1 + mypyc/lib-rt/static_data.h | 1 + mypyc/test-data/run-bytes.test | 6 +++--- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mypyc/lib-rt/static_data.c b/mypyc/lib-rt/static_data.c index c119bec56467..f723223823bf 100644 --- a/mypyc/lib-rt/static_data.c +++ b/mypyc/lib-rt/static_data.c @@ -46,6 +46,7 @@ intern_strings(void) { INTERN_STRING(clear, "clear"); INTERN_STRING(close_, "close"); INTERN_STRING(copy, "copy"); + INTERN_STRING(endswith, "endswith"); INTERN_STRING(keys, "keys"); INTERN_STRING(items, "items"); INTERN_STRING(join, "join"); diff --git a/mypyc/lib-rt/static_data.h b/mypyc/lib-rt/static_data.h index f779504b6c20..efc2e7fd2e78 100644 --- a/mypyc/lib-rt/static_data.h +++ b/mypyc/lib-rt/static_data.h @@ -38,6 +38,7 @@ typedef struct mypyc_interned_str_struct { PyObject *clear; PyObject *close_; PyObject *copy; + PyObject *endswith; PyObject *keys; PyObject *items; PyObject *join; diff --git a/mypyc/test-data/run-bytes.test b/mypyc/test-data/run-bytes.test index afb863a3b350..f40321a85ae2 100644 --- a/mypyc/test-data/run-bytes.test +++ b/mypyc/test-data/run-bytes.test @@ -238,9 +238,9 @@ def test_endswith() -> None: 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') + test2 = bytearray(b'some string') + assert test2.endswith(b'string') + assert not test2.endswith(b'other') [case testBytesSlicing] def test_bytes_slicing() -> None: