Skip to content
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
_(f"{'value'}")

# Don't trigger for t-strings
_(t"{'value'}")
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@

info(f"{name}")
info(f"{__name__}")

# Don't trigger for t-strings
info(t"{name}")
info(t"{__name__}")
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Original file line number Diff line number Diff line change
Expand Up @@ -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"")
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,10 @@ def test_division():
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""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a 100% clear to me if they should be excluded but I think it's a good starting point and it should be a way less common mistake and they would be a better fit for a assert-on-always-true or similar rule.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I meant by "cannot be empty". The actual documentation for this rule justifies it as:

An assert on a non-empty string literal will always pass, while an assert on an empty string literal will always fail.

which does not apply to t-strings.

assert t"hey"
assert t"{a}"
12 changes: 12 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pylint/no_self_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,15 @@ def unused_message(self):
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)

Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Comment on lines +37 to +39
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhat perversely someone could put __slots__ = t"abc" since t-strings are iterables that iterate over the string and interpolation objects within them. So that would accidentally be an iterable with a single string object "abc" in it.

If people are doing that (no way they are) we should probably implement a different lint rule for it though, that just says "why have you done this?"


def __init__(self, bar):
self.bar = bar

4 changes: 4 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyupgrade/UP012.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
4 changes: 4 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyupgrade/UP018.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,7 @@
int(1)and None
float(1.)and None
bool(True)and()


# t-strings are not native literals
str(t"hey")
Original file line number Diff line number Diff line change
@@ -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.
|
Expand Down
Original file line number Diff line number Diff line change
@@ -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
|
Original file line number Diff line number Diff line change
Expand Up @@ -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
|
Original file line number Diff line number Diff line change
@@ -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()`
|
Expand Down Expand Up @@ -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
|
Original file line number Diff line number Diff line change
Expand Up @@ -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}..."
|
Original file line number Diff line number Diff line change
Expand Up @@ -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)
|
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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")
Original file line number Diff line number Diff line change
Expand Up @@ -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)
|
Expand All @@ -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)
|
Expand All @@ -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
Loading