From 8cb1691ca038ca39942e088b956f5b94d8f636bf Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 16 Oct 2024 17:07:09 -0700 Subject: [PATCH] relax speedups str check --- CHANGES.rst | 2 ++ src/markupsafe/_speedups.c | 2 +- tests/test_escape.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6ca57f48..a1dce6b1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,8 @@ Version 3.1.0 Unreleased +- Fix compatibility when ``__str__`` returns a ``str`` subclass. :issue:`472` + Version 3.0.1 ------------- diff --git a/src/markupsafe/_speedups.c b/src/markupsafe/_speedups.c index 73c29558..09dd57ca 100644 --- a/src/markupsafe/_speedups.c +++ b/src/markupsafe/_speedups.c @@ -151,7 +151,7 @@ escape_unicode_kind4(PyUnicodeObject *in) static PyObject* escape_unicode(PyObject *self, PyObject *s) { - if (!PyUnicode_CheckExact(s)) + if (!PyUnicode_Check(s)) return NULL; // This check is no longer needed in Python 3.12. diff --git a/tests/test_escape.py b/tests/test_escape.py index 03e0b648..07698ec2 100644 --- a/tests/test_escape.py +++ b/tests/test_escape.py @@ -1,5 +1,7 @@ from __future__ import annotations +import typing as t + import pytest from markupsafe import escape @@ -30,3 +32,37 @@ ) def test_escape(value: str, expect: str) -> None: assert escape(value) == Markup(expect) + + +class Proxy: + def __init__(self, value: t.Any) -> None: + self.__value = value + + @property # type: ignore[misc] + def __class__(self) -> type[t.Any]: + # Make o.__class__ and isinstance(o, str) see the proxied object. + return self.__value.__class__ # type: ignore[no-any-return] + + def __str__(self) -> str: + return str(self.__value) + + +def test_proxy() -> None: + """Handle a proxy object that pretends its __class__ is str.""" + p = Proxy("test") + assert p.__class__ is str + assert isinstance(p, str) + assert escape(p) == Markup("test") + + +class ReferenceStr(str): + def __str__(self) -> str: + # This should return a str, but it returns the subclass instead. + return self + + +def test_subclass() -> None: + """Handle if str(o) does not return a plain str.""" + s = ReferenceStr("test") + assert isinstance(s, str) + assert escape(s) == Markup("test")