From 2a2cc3715889dabfbbbe868bf374f79311159c37 Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 14 Jul 2025 09:46:31 -0500 Subject: [PATCH] Add t-string fixtures for rules that do not need to be modified (#19146) I used a script to attempt to identify those rules with the following property: changing f-strings to t-strings in the corresponding fixture altered the number of lint errors emitted. In other words, those rules for which f-strings and t-strings are not treated the same in the current implementation. This PR documents the subset of such rules where this is fine and no changes need to be made to the implementation of the rule. Mostly these are the rules where it is relevant that an f-string evaluates to type `str` at runtime whereas t-strings do not. In theory many of these fixtures are not super necessary - it's unlikely t-strings would be used for most of these. However, the internal handling of t-strings is tightly coupled with that of f-strings, and may become even more so as we implement the upcoming changes due to https://github.com/python/cpython/pull/135996 . So I'd like to keep these around as regression tests. Note: The `flake8-bandit` fixtures were already added during the original t-string implementation. | Rule(s) | Reason | | --- | --- | | [`unused-method-argument` (`ARG002`)](https://docs.astral.sh/ruff/rules/unused-method-argument/#unused-method-argument-arg002) | f-strings exempted for msg in `NotImplementedError` not relevant for t-strings | | [`logging-f-string` (`G004`)](https://docs.astral.sh/ruff/rules/logging-f-string/#logging-f-string-g004) | t-strings cannot be used here | | [`f-string-in-get-text-func-call` (`INT001`)](https://docs.astral.sh/ruff/rules/f-string-in-get-text-func-call/#f-string-in-get-text-func-call-int001) | rule justified by eager evaluation of interpolations | | [`flake8-bandit`](https://docs.astral.sh/ruff/rules/#flake8-bandit-s)| rules justified by eager evaluation of interpolations | | [`single-string-slots` (`PLC0205`)](https://docs.astral.sh/ruff/rules/single-string-slots/#single-string-slots-plc0205) | t-strings cannot be slots in general | | [`unnecessary-encode-utf8` (`UP012`)](https://docs.astral.sh/ruff/rules/unnecessary-encode-utf8/#unnecessary-encode-utf8-up012) | cannot encode t-strings | | [`no-self-use` (`PLR6301`)](https://docs.astral.sh/ruff/rules/no-self-use/#no-self-use-plr6301) | f-strings exempted for msg in NotImplementedError not relevant for t-strings | | [`pytest-raises-too-broad` (`PT011`)](https://docs.astral.sh/ruff/rules/pytest-raises-too-broad/) / [`pytest-fail-without-message` (`PT016`)](https://docs.astral.sh/ruff/rules/pytest-fail-without-message/#pytest-fail-without-message-pt016) / [`pytest-warns-too-broad` (`PT030`)](https://docs.astral.sh/ruff/rules/pytest-warns-too-broad/#pytest-warns-too-broad-pt030) | t-strings cannot be empty or used as messages | | [`assert-on-string-literal` (`PLW0129`)](https://docs.astral.sh/ruff/rules/assert-on-string-literal/#assert-on-string-literal-plw0129) | t-strings are not strings and cannot be empty | | [`native-literals` (`UP018`)](https://docs.astral.sh/ruff/rules/native-literals/#native-literals-up018) | t-strings are not native literals | --- .../test/fixtures/flake8_gettext/INT001.py | 3 +++ .../fixtures/flake8_logging_format/G004.py | 4 ++++ .../test/fixtures/flake8_pytest_style/PT011.py | 4 ++++ .../test/fixtures/flake8_pytest_style/PT016.py | 6 ++++++ .../test/fixtures/flake8_pytest_style/PT030.py | 4 ++++ .../fixtures/flake8_unused_arguments/ARG.py | 11 +++++++++++ .../pylint/assert_on_string_literal.py | 7 +++++++ .../test/fixtures/pylint/no_self_use.py | 12 ++++++++++++ .../fixtures/pylint/single_string_slots.py | 8 ++++++++ .../resources/test/fixtures/pyupgrade/UP012.py | 4 ++++ .../resources/test/fixtures/pyupgrade/UP018.py | 4 ++++ ...s__flake8_bugbear__tests__B018_B018.py.snap | 1 - ...string-in-get-text-func-call_INT001.py.snap | 3 ++- ..._flake8_logging_format__tests__G004.py.snap | 2 ++ ...les__flake8_pytest_style__tests__PT016.snap | 3 ++- ...unused_arguments__tests__ARG002_ARG.py.snap | 10 ++++++++++ ..._pylint__tests__PLR6301_no_self_use.py.snap | 18 ++++++++++++++++++ ...ter__rules__pyupgrade__tests__UP012.py.snap | 5 +++++ ...ter__rules__pyupgrade__tests__UP018.py.snap | 6 ++++++ 19 files changed, 112 insertions(+), 3 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_gettext/INT001.py b/crates/ruff_linter/resources/test/fixtures/flake8_gettext/INT001.py index 703ad7df4b..240213f485 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_gettext/INT001.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_gettext/INT001.py @@ -1 +1,4 @@ _(f"{'value'}") + +# Don't trigger for t-strings +_(t"{'value'}") diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_logging_format/G004.py b/crates/ruff_linter/resources/test/fixtures/flake8_logging_format/G004.py index e339006435..da05aba630 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_logging_format/G004.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_logging_format/G004.py @@ -13,3 +13,7 @@ from logging import info info(f"{name}") info(f"{__name__}") + +# Don't trigger for t-strings +info(t"{name}") +info(t"{__name__}") diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT011.py b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT011.py index 368cea6af6..e524935473 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT011.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT011.py @@ -47,3 +47,7 @@ def test_error_match_is_empty(): with pytest.raises(ValueError, match=f""): raise ValueError("Can't divide 1 by 0") + +def test_ok_t_string_match(): + with pytest.raises(ValueError, match=t""): + raise ValueError("Can't divide 1 by 0") diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT016.py b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT016.py index 288d5d9be6..0a8f6bc8fb 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT016.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT016.py @@ -23,3 +23,9 @@ def f(): pytest.fail(msg=f"") pytest.fail(reason="") pytest.fail(reason=f"") + +# Skip for t-strings +def g(): + pytest.fail(t"") + pytest.fail(msg=t"") + pytest.fail(reason=t"") diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT030.py b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT030.py index 129f1f0442..2db3885b6d 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT030.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT030.py @@ -32,3 +32,7 @@ def test_error_match_is_empty(): with pytest.warns(UserWarning, match=f""): pass + +def test_ok_match_t_string(): + with pytest.warns(UserWarning, match=t""): + pass diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_unused_arguments/ARG.py b/crates/ruff_linter/resources/test/fixtures/flake8_unused_arguments/ARG.py index 7cfb3fceee..311409e6ff 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_unused_arguments/ARG.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_unused_arguments/ARG.py @@ -245,3 +245,14 @@ def f(bar: str): class C: def __init__(self, x) -> None: print(locals()) + +### +# Should trigger for t-string here +# even though the corresponding f-string +# does not trigger (since it is common in stubs) +### +class C: + def f(self, x, y): + """Docstring.""" + msg = t"{x}..." + raise NotImplementedError(msg) diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/assert_on_string_literal.py b/crates/ruff_linter/resources/test/fixtures/pylint/assert_on_string_literal.py index 4c572c2767..380b502647 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/assert_on_string_literal.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/assert_on_string_literal.py @@ -22,3 +22,10 @@ assert b"hello" # [assert-on-string-literal] assert "", b"hi" # [assert-on-string-literal] assert "WhyNotHere?", "HereIsOk" # [assert-on-string-literal] assert 12, "ok here" + + +# t-strings are always True even when "empty" +# skip lint in this case +assert t"" +assert t"hey" +assert t"{a}" diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/no_self_use.py b/crates/ruff_linter/resources/test/fixtures/pylint/no_self_use.py index 34c4709505..c246792074 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/no_self_use.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/no_self_use.py @@ -140,3 +140,15 @@ class Foo: def unused_message_2(self, x): msg = "" raise NotImplementedError(x) + +class TPerson: + def developer_greeting(self, name): # [no-self-use] + print(t"Greetings {name}!") + + def greeting_1(self): + print(t"Hello from {self.name} !") + + def tstring(self, x): + msg = t"{x}" + raise NotImplementedError(msg) + diff --git a/crates/ruff_linter/resources/test/fixtures/pylint/single_string_slots.py b/crates/ruff_linter/resources/test/fixtures/pylint/single_string_slots.py index b7a2dac915..58d6381390 100644 --- a/crates/ruff_linter/resources/test/fixtures/pylint/single_string_slots.py +++ b/crates/ruff_linter/resources/test/fixtures/pylint/single_string_slots.py @@ -33,3 +33,11 @@ class Foo: def __init__(self, bar): self.bar = bar + +# This is a type error, out of scope for the rule +class Foo: + __slots__ = t"bar{baz}" + + def __init__(self, bar): + self.bar = bar + diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP012.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP012.py index 0029b89e9b..2b7fdfa288 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP012.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP012.py @@ -84,3 +84,7 @@ def _match_ignore(line): # Not a valid type annotation but this test shouldn't result in a panic. # Refer: https://github.com/astral-sh/ruff/issues/11736 x: '"foo".encode("utf-8")' + +# AttributeError for t-strings so skip lint +(t"foo{bar}").encode("utf-8") +(t"foo{bar}").encode(encoding="utf-8") diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py index a5b7e1d894..86b8d9aebf 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py @@ -90,3 +90,7 @@ bool(True)and None int(1)and None float(1.)and None bool(True)and() + + +# t-strings are not native literals +str(t"hey") diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B018_B018.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B018_B018.py.snap index d455292d93..5eee755b4a 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B018_B018.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B018_B018.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs -snapshot_kind: text --- B018.py:11:5: B018 Found useless expression. Either assign it to a variable or remove it. | diff --git a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__f-string-in-get-text-func-call_INT001.py.snap b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__f-string-in-get-text-func-call_INT001.py.snap index 90495ebbad..81d2c6b328 100644 --- a/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__f-string-in-get-text-func-call_INT001.py.snap +++ b/crates/ruff_linter/src/rules/flake8_gettext/snapshots/ruff_linter__rules__flake8_gettext__tests__f-string-in-get-text-func-call_INT001.py.snap @@ -1,9 +1,10 @@ --- source: crates/ruff_linter/src/rules/flake8_gettext/mod.rs -snapshot_kind: text --- INT001.py:1:3: INT001 f-string is resolved before function call; consider `_("string %s") % arg` | 1 | _(f"{'value'}") | ^^^^^^^^^^^^ INT001 +2 | +3 | # Don't trigger for t-strings | diff --git a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004.py.snap b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004.py.snap index 9cd4524dd9..bb51dc65f4 100644 --- a/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004.py.snap +++ b/crates/ruff_linter/src/rules/flake8_logging_format/snapshots/ruff_linter__rules__flake8_logging_format__tests__G004.py.snap @@ -52,4 +52,6 @@ G004.py:15:6: G004 Logging statement uses f-string 14 | info(f"{name}") 15 | info(f"{__name__}") | ^^^^^^^^^^^^^ G004 +16 | +17 | # Don't trigger for t-strings | diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT016.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT016.snap index ac5c1f3a36..4730ecdeca 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT016.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT016.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs -snapshot_kind: text --- PT016.py:19:5: PT016 No message passed to `pytest.fail()` | @@ -67,4 +66,6 @@ PT016.py:25:5: PT016 No message passed to `pytest.fail()` 24 | pytest.fail(reason="") 25 | pytest.fail(reason=f"") | ^^^^^^^^^^^ PT016 +26 | +27 | # Skip for t-strings | diff --git a/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG002_ARG.py.snap b/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG002_ARG.py.snap index 3e1e19260a..2c7fe70056 100644 --- a/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG002_ARG.py.snap +++ b/crates/ruff_linter/src/rules/flake8_unused_arguments/snapshots/ruff_linter__rules__flake8_unused_arguments__tests__ARG002_ARG.py.snap @@ -66,3 +66,13 @@ ARG.py:216:24: ARG002 Unused method argument: `x` | ^ ARG002 217 | print("Hello, world!") | + +ARG.py:255:20: ARG002 Unused method argument: `y` + | +253 | ### +254 | class C: +255 | def f(self, x, y): + | ^ ARG002 +256 | """Docstring.""" +257 | msg = t"{x}..." + | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR6301_no_self_use.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR6301_no_self_use.py.snap index 74818fdb39..4971a0c1fb 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR6301_no_self_use.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLR6301_no_self_use.py.snap @@ -75,3 +75,21 @@ no_self_use.py:140:9: PLR6301 Method `unused_message_2` could be a function, cla 141 | msg = "" 142 | raise NotImplementedError(x) | + +no_self_use.py:145:9: PLR6301 Method `developer_greeting` could be a function, class method, or static method + | +144 | class TPerson: +145 | def developer_greeting(self, name): # [no-self-use] + | ^^^^^^^^^^^^^^^^^^ PLR6301 +146 | print(t"Greetings {name}!") + | + +no_self_use.py:151:9: PLR6301 Method `tstring` could be a function, class method, or static method + | +149 | print(t"Hello from {self.name} !") +150 | +151 | def tstring(self, x): + | ^^^^^^^ PLR6301 +152 | msg = t"{x}" +153 | raise NotImplementedError(msg) + | diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP012.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP012.py.snap index 99b2f05d6e..a3e42b982f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP012.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP012.py.snap @@ -576,6 +576,8 @@ UP012.py:86:5: UP012 [*] Unnecessary call to `encode` as UTF-8 85 | # Refer: https://github.com/astral-sh/ruff/issues/11736 86 | x: '"foo".encode("utf-8")' | ^^^^^^^^^^^^^^^^^^^^^ UP012 +87 | +88 | # AttributeError for t-strings so skip lint | = help: Rewrite as bytes literal @@ -585,3 +587,6 @@ UP012.py:86:5: UP012 [*] Unnecessary call to `encode` as UTF-8 85 85 | # Refer: https://github.com/astral-sh/ruff/issues/11736 86 |-x: '"foo".encode("utf-8")' 86 |+x: 'b"foo"' +87 87 | +88 88 | # AttributeError for t-strings so skip lint +89 89 | (t"foo{bar}").encode("utf-8") diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018.py.snap index d57eac6e12..0f6820140c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP018.py.snap @@ -660,6 +660,7 @@ UP018.py:90:1: UP018 [*] Unnecessary `int` call (rewrite as a literal) 90 |+1 and None 91 91 | float(1.)and None 92 92 | bool(True)and() +93 93 | UP018.py:91:1: UP018 [*] Unnecessary `float` call (rewrite as a literal) | @@ -678,6 +679,8 @@ UP018.py:91:1: UP018 [*] Unnecessary `float` call (rewrite as a literal) 91 |-float(1.)and None 91 |+1. and None 92 92 | bool(True)and() +93 93 | +94 94 | UP018.py:92:1: UP018 [*] Unnecessary `bool` call (rewrite as a literal) | @@ -694,3 +697,6 @@ UP018.py:92:1: UP018 [*] Unnecessary `bool` call (rewrite as a literal) 91 91 | float(1.)and None 92 |-bool(True)and() 92 |+True and() +93 93 | +94 94 | +95 95 | # t-strings are not native literals