From 10bcc70ad6f5eff8af023d662682e643f0dda4aa Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Fri, 29 Nov 2024 19:59:40 +0100 Subject: [PATCH 1/3] [pre-commit] Upgrade ruff to 0.8.1 --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 ++ src/_pytest/_code/__init__.py | 8 ++--- src/_pytest/_code/code.py | 10 +++---- src/_pytest/assertion/rewrite.py | 4 +-- src/_pytest/capture.py | 4 +-- src/_pytest/fixtures.py | 2 +- src/_pytest/mark/expression.py | 10 +++---- src/_pytest/mark/structures.py | 8 ++--- src/_pytest/nodes.py | 10 +++---- src/_pytest/python.py | 8 ++--- src/_pytest/runner.py | 2 +- src/_pytest/skipping.py | 2 +- src/pytest/__init__.py | 50 ++++++++++++++++---------------- testing/_py/test_local.py | 4 +-- 15 files changed, 61 insertions(+), 65 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb02fd0f00f..8a5418350d7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.7.4" + rev: "v0.8.1" hooks: - id: ruff args: ["--fix"] diff --git a/pyproject.toml b/pyproject.toml index dce6a0870e1..b10c1289b80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -156,8 +156,10 @@ lint.ignore = [ "PLW0120", # remove the else and dedent its contents "PLW0603", # Using the global statement "PLW2901", # for loop variable overwritten by assignment target + "PYI063", # ruff ignore "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "UP031", ] lint.per-file-ignores."src/_pytest/_py/**/*.py" = [ "B", diff --git a/src/_pytest/_code/__init__.py b/src/_pytest/_code/__init__.py index 0bfde42604d..7f67a2e3e0a 100644 --- a/src/_pytest/_code/__init__.py +++ b/src/_pytest/_code/__init__.py @@ -16,11 +16,11 @@ __all__ = [ "Code", "ExceptionInfo", - "filter_traceback", "Frame", - "getfslineno", - "getrawcode", + "Source", "Traceback", "TracebackEntry", - "Source", + "filter_traceback", + "getfslineno", + "getrawcode", ] diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index b98ce013b6e..bb09c01c11d 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -953,7 +953,7 @@ def repr_traceback_entry( if short: message = f"in {entry.name}" else: - message = excinfo and excinfo.typename or "" + message = (excinfo and excinfo.typename) or "" entry_path = entry.path path = self._makepath(entry_path) reprfileloc = ReprFileLocation(path, entry.lineno + 1, message) @@ -1182,10 +1182,8 @@ def toterminal(self, tw: TerminalWriter) -> None: entry.toterminal(tw) if i < len(self.reprentries) - 1: next_entry = self.reprentries[i + 1] - if ( - entry.style == "long" - or entry.style == "short" - and next_entry.style == "long" + if entry.style == "long" or ( + entry.style == "short" and next_entry.style == "long" ): tw.sep(self.entrysep) @@ -1369,7 +1367,7 @@ def getfslineno(obj: object) -> tuple[str | Path, int]: except TypeError: return "", -1 - fspath = fn and absolutepath(fn) or "" + fspath = (fn and absolutepath(fn)) or "" lineno = -1 if fspath: try: diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 93a08a4e69f..c414b30a4a8 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -53,7 +53,7 @@ class Sentinel: # pytest caches rewritten pycs in pycache dirs PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}" -PYC_EXT = ".py" + (__debug__ and "c" or "o") +PYC_EXT = ".py" + ((__debug__ and "c") or "o") PYC_TAIL = "." + PYTEST_TAG + PYC_EXT # Special marker that denotes we have just left a scope definition @@ -481,7 +481,7 @@ def _should_repr_global_name(obj: object) -> bool: def _format_boolop(explanations: Iterable[str], is_or: bool) -> str: - explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")" + explanation = "(" + ((is_or and " or ") or " and ").join(explanations) + ")" return explanation.replace("%", "%%") diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 93a3b04182e..de2ee9c8dbd 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -360,7 +360,7 @@ def repr(self, class_name: str) -> str: return "<{} {} _old={} _state={!r} tmpfile={!r}>".format( class_name, self.name, - hasattr(self, "_old") and repr(self._old) or "", + (hasattr(self, "_old") and repr(self._old)) or "", self._state, self.tmpfile, ) @@ -369,7 +369,7 @@ def __repr__(self) -> str: return "<{} {} _old={} _state={!r} tmpfile={!r}>".format( self.__class__.__name__, self.name, - hasattr(self, "_old") and repr(self._old) or "", + (hasattr(self, "_old") and repr(self._old)) or "", self._state, self.tmpfile, ) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 01707418755..95fa1336169 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -305,7 +305,7 @@ class FuncFixtureInfo: these are not reflected here. """ - __slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs") + __slots__ = ("argnames", "initialnames", "name2fixturedefs", "names_closure") # Fixture names that the item requests directly by function parameters. argnames: tuple[str, ...] diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index d0ab190e3b6..b71ed29c62f 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -58,7 +58,7 @@ class TokenType(enum.Enum): @dataclasses.dataclass(frozen=True) class Token: - __slots__ = ("type", "value", "pos") + __slots__ = ("pos", "type", "value") type: TokenType value: str pos: int @@ -80,7 +80,7 @@ def __str__(self) -> str: class Scanner: - __slots__ = ("tokens", "current") + __slots__ = ("current", "tokens") def __init__(self, input: str) -> None: self.tokens = self.lex(input) @@ -238,10 +238,8 @@ def single_kwarg(s: Scanner) -> ast.keyword: value: str | int | bool | None = value_token.value[1:-1] # strip quotes else: value_token = s.accept(TokenType.IDENT, reject=True) - if ( - (number := value_token.value).isdigit() - or number.startswith("-") - and number[1:].isdigit() + if (number := value_token.value).isdigit() or ( + number.startswith("-") and number[1:].isdigit() ): value = int(number) elif value_token.value in BUILTIN_MATCHERS: diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index d1e0a49b62d..e7e32555ba4 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -562,7 +562,7 @@ def __getattr__(self, name: str) -> MarkDecorator: @final class NodeKeywords(MutableMapping[str, Any]): - __slots__ = ("node", "parent", "_markers") + __slots__ = ("_markers", "node", "parent") def __init__(self, node: Node) -> None: self.node = node @@ -584,10 +584,8 @@ def __setitem__(self, key: str, value: Any) -> None: # below and use the collections.abc fallback, but that would be slow. def __contains__(self, key: object) -> bool: - return ( - key in self._markers - or self.parent is not None - and key in self.parent.keywords + return key in self._markers or ( + self.parent is not None and key in self.parent.keywords ) def update( # type: ignore[override] diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 5b50fbc92cb..0d1f3d2352b 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -143,14 +143,14 @@ class Node(abc.ABC, metaclass=NodeMeta): # Use __slots__ to make attribute access faster. # Note that __dict__ is still available. __slots__ = ( + "__dict__", + "_nodeid", + "_store", + "config", "name", "parent", - "config", - "session", "path", - "_nodeid", - "_store", - "__dict__", + "session", ) def __init__( diff --git a/src/_pytest/python.py b/src/_pytest/python.py index f75bb5b432e..0b33133c1f2 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -431,7 +431,7 @@ def _genfunctions(self, name: str, funcobj) -> Iterator[Function]: assert modulecol is not None module = modulecol.obj clscol = self.getparent(Class) - cls = clscol and clscol.obj or None + cls = (clscol and clscol.obj) or None definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj) fixtureinfo = definition._fixtureinfo @@ -848,12 +848,12 @@ class IdMaker: __slots__ = ( "argnames", - "parametersets", + "config", + "func_name", "idfn", "ids", - "config", "nodeid", - "func_name", + "parametersets", ) # The argnames of the parametrization. diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 5189efce538..d2b7fda8c2a 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -533,7 +533,7 @@ def teardown_exact(self, nextitem: Item | None) -> None: When nextitem is None (meaning we're at the last item), the entire stack is torn down. """ - needed_collectors = nextitem and nextitem.listchain() or [] + needed_collectors = (nextitem and nextitem.listchain()) or [] exceptions: list[BaseException] = [] while self.stack: if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 8fa17a01eb0..d21be181955 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -196,7 +196,7 @@ def evaluate_skip_marks(item: Item) -> Skip | None: class Xfail: """The result of evaluate_xfail_marks().""" - __slots__ = ("reason", "run", "strict", "raises") + __slots__ = ("raises", "reason", "run", "strict") reason: str run: bool diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 5ab2a22b0c0..f0c3516f4cc 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -88,41 +88,27 @@ __all__ = [ - "__version__", - "approx", "Cache", "CallInfo", "CaptureFixture", "Class", - "cmdline", - "Collector", "CollectReport", + "Collector", "Config", - "console_main", - "deprecated_call", "Dir", "Directory", "DoctestItem", - "exit", "ExceptionInfo", "ExitCode", - "fail", "File", - "fixture", "FixtureDef", "FixtureLookupError", "FixtureRequest", - "freeze_includes", "Function", - "hookimpl", "HookRecorder", - "hookspec", - "importorskip", "Item", "LineMatcher", "LogCaptureFixture", - "main", - "mark", "Mark", "MarkDecorator", "MarkGenerator", @@ -131,7 +117,6 @@ "MonkeyPatch", "OptionGroup", "Package", - "param", "Parser", "PytestAssertRewriteWarning", "PytestCacheWarning", @@ -139,31 +124,46 @@ "PytestConfigWarning", "PytestDeprecationWarning", "PytestExperimentalApiWarning", - "PytestRemovedIn9Warning", - "Pytester", "PytestPluginManager", + "PytestRemovedIn9Warning", "PytestUnhandledThreadExceptionWarning", "PytestUnknownMarkWarning", "PytestUnraisableExceptionWarning", "PytestWarning", - "raises", + "Pytester", "RecordedHookCall", - "register_assert_rewrite", "RunResult", "Session", - "set_trace", - "skip", "Stash", "StashKey", - "version_tuple", - "TempdirFactory", "TempPathFactory", + "TempdirFactory", "TerminalReporter", - "Testdir", "TestReport", "TestShortLogReport", + "Testdir", "UsageError", "WarningsRecorder", + "__version__", + "approx", + "cmdline", + "console_main", + "deprecated_call", + "exit", + "fail", + "fixture", + "freeze_includes", + "hookimpl", + "hookspec", + "importorskip", + "main", + "mark", + "param", + "raises", + "register_assert_rewrite", + "set_trace", + "skip", + "version_tuple", "warns", "xfail", "yield_fixture", diff --git a/testing/_py/test_local.py b/testing/_py/test_local.py index 21fbfb3e3ad..b6d49c5425e 100644 --- a/testing/_py/test_local.py +++ b/testing/_py/test_local.py @@ -855,7 +855,7 @@ def test_fnmatch_file_abspath(self, tmpdir): assert b.fnmatch(pattern) def test_sysfind(self): - name = sys.platform == "win32" and "cmd" or "test" + name = (sys.platform == "win32" and "cmd") or "test" x = local.sysfind(name) assert x.check(file=1) assert local.sysfind("jaksdkasldqwe") is None @@ -1250,7 +1250,7 @@ def test_owner_group_not_implemented(self, path1): def test_chmod_simple_int(self, path1): mode = path1.stat().mode # Ensure that we actually change the mode to something different. - path1.chmod(mode == 0 and 1 or 0) + path1.chmod((mode == 0 and 1) or 0) try: print(path1.stat().mode) print(mode) From 049bb29d8a7fce42bcb4825c23bacffb56b68e55 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Thu, 28 Nov 2024 22:17:12 +0100 Subject: [PATCH 2/3] [Fix PYI063] Use PEP 570 syntax for positional-only parameters --- pyproject.toml | 1 - .../dataclasses/test_compare_dataclasses_with_custom_eq.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b10c1289b80..c5833f73851 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -156,7 +156,6 @@ lint.ignore = [ "PLW0120", # remove the else and dedent its contents "PLW0603", # Using the global statement "PLW2901", # for loop variable overwritten by assignment target - "PYI063", # ruff ignore "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` "UP031", diff --git a/testing/example_scripts/dataclasses/test_compare_dataclasses_with_custom_eq.py b/testing/example_scripts/dataclasses/test_compare_dataclasses_with_custom_eq.py index b787cb39ee2..e25bebf74dd 100644 --- a/testing/example_scripts/dataclasses/test_compare_dataclasses_with_custom_eq.py +++ b/testing/example_scripts/dataclasses/test_compare_dataclasses_with_custom_eq.py @@ -10,7 +10,7 @@ class SimpleDataObject: field_a: int = field() field_b: str = field() - def __eq__(self, __o: object) -> bool: + def __eq__(self, __o: object, /) -> bool: return super().__eq__(__o) left = SimpleDataObject(1, "b") From 17c5bbbdaee125f7d574ae390d4807724efa871a Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Fri, 29 Nov 2024 20:30:17 +0100 Subject: [PATCH 3/3] [Fix UP031] Manually, keeping some required %r specifiers Co-authored-by: Bruno Oliveira --- bench/empty.py | 2 +- pyproject.toml | 1 - src/_pytest/_code/code.py | 9 ++------ src/_pytest/_io/pprint.py | 2 +- src/_pytest/_py/error.py | 2 +- src/_pytest/assertion/util.py | 9 +++----- src/_pytest/cacheprovider.py | 6 ++--- src/_pytest/compat.py | 4 ++-- src/_pytest/doctest.py | 2 +- src/_pytest/fixtures.py | 2 +- src/_pytest/main.py | 18 +++++++-------- src/_pytest/mark/structures.py | 10 ++------- src/_pytest/pytester.py | 6 +++-- src/_pytest/terminal.py | 22 +++++++++---------- testing/_py/test_local.py | 4 ++-- ...test_compare_dataclasses_with_custom_eq.py | 4 ++-- testing/python/metafunc.py | 7 +++--- testing/test_assertion.py | 7 +++--- testing/test_conftest.py | 4 ++-- testing/test_doctest.py | 2 +- testing/test_terminal.py | 2 +- 21 files changed, 53 insertions(+), 72 deletions(-) diff --git a/bench/empty.py b/bench/empty.py index 35abeef4140..346b79d5e33 100644 --- a/bench/empty.py +++ b/bench/empty.py @@ -2,4 +2,4 @@ for i in range(1000): - exec("def test_func_%d(): pass" % i) + exec(f"def test_func_{i}(): pass") diff --git a/pyproject.toml b/pyproject.toml index c5833f73851..dce6a0870e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -158,7 +158,6 @@ lint.ignore = [ "PLW2901", # for loop variable overwritten by assignment target # ruff ignore "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` - "UP031", ] lint.per-file-ignores."src/_pytest/_py/**/*.py" = [ "B", diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index bb09c01c11d..bba8896076e 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -217,7 +217,7 @@ def relline(self) -> int: return self.lineno - self.frame.code.firstlineno def __repr__(self) -> str: - return "" % (self.frame.code.path, self.lineno + 1) + return f"" @property def statement(self) -> Source: @@ -303,12 +303,7 @@ def __str__(self) -> str: # This output does not quite match Python's repr for traceback entries, # but changing it to do so would break certain plugins. See # https://github.com/pytest-dev/pytest/pull/7535/ for details. - return " File %r:%d in %s\n %s\n" % ( - str(self.path), - self.lineno + 1, - name, - line, - ) + return f" File '{self.path}':{self.lineno+1} in {name}\n {line}\n" @property def name(self) -> str: diff --git a/src/_pytest/_io/pprint.py b/src/_pytest/_io/pprint.py index ca780c41344..28f06909206 100644 --- a/src/_pytest/_io/pprint.py +++ b/src/_pytest/_io/pprint.py @@ -540,7 +540,7 @@ def _pprint_deque( ) -> None: stream.write(object.__class__.__name__ + "(") if object.maxlen is not None: - stream.write("maxlen=%d, " % object.maxlen) + stream.write(f"maxlen={object.maxlen}, ") stream.write("[") self._format_items(object, stream, indent, allowance + 1, context, level) diff --git a/src/_pytest/_py/error.py b/src/_pytest/_py/error.py index 3a63304008a..de0c04a4838 100644 --- a/src/_pytest/_py/error.py +++ b/src/_pytest/_py/error.py @@ -69,7 +69,7 @@ def _geterrnoclass(self, eno: int) -> type[Error]: try: return self._errno2class[eno] except KeyError: - clsname = errno.errorcode.get(eno, "UnknownErrno%d" % (eno,)) + clsname = errno.errorcode.get(eno, f"UnknownErrno{eno}") errorcls = type( clsname, (Error,), diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 0db846ce204..3fe7eb9d862 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -406,8 +406,7 @@ def _compare_eq_sequence( ] else: explanation += [ - "%s contains %d more items, first extra item: %s" - % (dir_with_more, len_diff, highlighter(extra)) + f"{dir_with_more} contains {len_diff} more items, first extra item: {highlighter(extra)}" ] return explanation @@ -510,8 +509,7 @@ def _compare_eq_dict( len_extra_left = len(extra_left) if len_extra_left: explanation.append( - "Left contains %d more item%s:" - % (len_extra_left, "" if len_extra_left == 1 else "s") + f"Left contains {len_extra_left} more item{'' if len_extra_left == 1 else 's'}:" ) explanation.extend( highlighter(pprint.pformat({k: left[k] for k in extra_left})).splitlines() @@ -520,8 +518,7 @@ def _compare_eq_dict( len_extra_right = len(extra_right) if len_extra_right: explanation.append( - "Right contains %d more item%s:" - % (len_extra_right, "" if len_extra_right == 1 else "s") + f"Right contains {len_extra_right} more item{'' if len_extra_right == 1 else 's'}:" ) explanation.extend( highlighter(pprint.pformat({k: right[k] for k in extra_right})).splitlines() diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index bf643d6f4dc..facb98f09e0 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -388,8 +388,8 @@ def pytest_collection_modifyitems( if not previously_failed: # Running a subset of all tests with recorded failures # only outside of it. - self._report_status = "%d known failures not in selected tests" % ( - len(self.lastfailed), + self._report_status = ( + f"{len(self.lastfailed)} known failures not in selected tests" ) else: if self.config.getoption("lf"): @@ -622,5 +622,5 @@ def cacheshow(config: Config, session: Session) -> int: # print("%s/" % p.relative_to(basedir)) if p.is_file(): key = str(p.relative_to(basedir)) - tw.line(f"{key} is a file of length {p.stat().st_size:d}") + tw.line(f"{key} is a file of length {p.stat().st_size}") return 0 diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 2f7413d466a..1aa7495bddb 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -71,8 +71,8 @@ def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str: except ValueError: pass else: - return "%s:%d" % (relfn, lineno + 1) - return "%s:%d" % (fn, lineno + 1) + return f"{relfn}:{lineno+1}" + return f"{fn}:{lineno+1}" def num_mock_patch_args(function) -> int: diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 598df84d70e..0dbef6056d7 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -353,7 +353,7 @@ def repr_failure( # type: ignore[override] # add line numbers to the left of the error message assert test.lineno is not None lines = [ - "%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines) + f"{i + test.lineno + 1:03d} {x}" for (i, x) in enumerate(lines) ] # trim docstring error lines to 10 lines = lines[max(example.lineno - 9, 0) : example.lineno + 1] diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 95fa1336169..76c4b919aff 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -884,7 +884,7 @@ def toterminal(self, tw: TerminalWriter) -> None: red=True, ) tw.line() - tw.line("%s:%d" % (os.fspath(self.filename), self.firstlineno + 1)) + tw.line(f"{os.fspath(self.filename)}:{self.firstlineno + 1}") def call_fixture_func( diff --git a/src/_pytest/main.py b/src/_pytest/main.py index d7086537f39..c97f746e1d6 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -349,8 +349,7 @@ def pytest_collection(session: Session) -> None: def pytest_runtestloop(session: Session) -> bool: if session.testsfailed and not session.config.option.continue_on_collection_errors: raise session.Interrupted( - "%d error%s during collection" - % (session.testsfailed, "s" if session.testsfailed != 1 else "") + f"{session.testsfailed} error{'s' if session.testsfailed != 1 else ''} during collection" ) if session.config.option.collectonly: @@ -585,13 +584,12 @@ def from_config(cls, config: Config) -> Session: return session def __repr__(self) -> str: - return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % ( - self.__class__.__name__, - self.name, - getattr(self, "exitstatus", ""), - self.testsfailed, - self.testscollected, - ) + return ( + f"<{self.__class__.__name__} {self.name} " + f"exitstatus=%r " + f"testsfailed={self.testsfailed} " + f"testscollected={self.testscollected}>" + ) % getattr(self, "exitstatus", "") @property def shouldstop(self) -> bool | str: @@ -654,7 +652,7 @@ def pytest_runtest_logreport(self, report: TestReport | CollectReport) -> None: self.testsfailed += 1 maxfail = self.config.getvalue("maxfail") if maxfail and self.testsfailed >= maxfail: - self.shouldfail = "stopping after %d failures" % (self.testsfailed) + self.shouldfail = f"stopping after {self.testsfailed} failures" pytest_collectreport = pytest_runtest_logreport diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index e7e32555ba4..ac64ef2d606 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -48,13 +48,7 @@ def get_empty_parameterset_mark( from ..nodes import Collector fs, lineno = getfslineno(func) - reason = "got empty parameter set %r, function %s at %s:%d" % ( - argnames, - func.__name__, - fs, - lineno, - ) - + reason = f"got empty parameter set {argnames!r}, function {func.__name__} at {fs}:{lineno}" requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) if requested_mark in ("", None, "skip"): mark = MARK_GEN.skip(reason=reason) @@ -64,7 +58,7 @@ def get_empty_parameterset_mark( f_name = func.__name__ _, lineno = getfslineno(func) raise Collector.CollectError( - "Empty parameter set in '%s' at line %d" % (f_name, lineno + 1) + f"Empty parameter set in '{f_name}' at line {lineno + 1}" ) else: raise LookupError(requested_mark) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 056be52a4ed..412d850d2da 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -547,8 +547,10 @@ def __init__( def __repr__(self) -> str: return ( - "" - % (self.ret, len(self.stdout.lines), len(self.stderr.lines), self.duration) + f"" ) def parseoutcomes(self) -> dict[str, int]: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index a74f73bff17..348a78af9f5 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -766,13 +766,13 @@ def report_collect(self, final: bool = False) -> None: str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s") ) if errors: - line += " / %d error%s" % (errors, "s" if errors != 1 else "") + line += f" / {errors} error{'s' if errors != 1 else ''}" if deselected: - line += " / %d deselected" % deselected + line += f" / {deselected} deselected" if skipped: - line += " / %d skipped" % skipped + line += f" / {skipped} skipped" if self._numcollected > selected: - line += " / %d selected" % selected + line += f" / {selected} selected" if self.isatty: self.rewrite(line, bold=True, erase=True) if final: @@ -862,7 +862,7 @@ def _printcollecteditems(self, items: Sequence[Item]) -> None: if test_cases_verbosity < -1: counts = Counter(item.nodeid.split("::", 1)[0] for item in items) for name, count in sorted(counts.items()): - self._tw.line("%s: %d" % (name, count)) + self._tw.line(f"{name}: {count}") else: for item in items: self._tw.line(item.nodeid) @@ -1254,11 +1254,9 @@ def show_skipped_folded(lines: list[str]) -> None: if reason.startswith(prefix): reason = reason[len(prefix) :] if lineno is not None: - lines.append( - "%s [%d] %s:%d: %s" % (markup_word, num, fspath, lineno, reason) - ) + lines.append(f"{markup_word} [{num}] {fspath}:{lineno}: {reason}") else: - lines.append("%s [%d] %s: %s" % (markup_word, num, fspath, reason)) + lines.append(f"{markup_word} [{num}] {fspath}: {reason}") def show_skipped_unfolded(lines: list[str]) -> None: skipped: list[CollectReport] = self.stats.get("skipped", []) @@ -1375,7 +1373,7 @@ def _build_normal_summary_stats_line( count = len(reports) color = _color_for_type.get(key, _color_for_type_default) markup = {color: True, "bold": color == main_color} - parts.append(("%d %s" % pluralize(count, key), markup)) + parts.append(("%d %s" % pluralize(count, key), markup)) # noqa: UP031 if not parts: parts = [("no tests ran", {_color_for_type_default: True})] @@ -1394,7 +1392,7 @@ def _build_collect_only_summary_stats_line( elif deselected == 0: main_color = "green" - collected_output = "%d %s collected" % pluralize(self._numcollected, "test") + collected_output = "%d %s collected" % pluralize(self._numcollected, "test") # noqa: UP031 parts = [(collected_output, {main_color: True})] else: all_tests_were_deselected = self._numcollected == deselected @@ -1410,7 +1408,7 @@ def _build_collect_only_summary_stats_line( if errors: main_color = _color_for_type["error"] - parts += [("%d %s" % pluralize(errors, "error"), {main_color: True})] + parts += [("%d %s" % pluralize(errors, "error"), {main_color: True})] # noqa: UP031 return parts, main_color diff --git a/testing/_py/test_local.py b/testing/_py/test_local.py index b6d49c5425e..cd8752fb79b 100644 --- a/testing/_py/test_local.py +++ b/testing/_py/test_local.py @@ -948,7 +948,7 @@ def test_make_numbered_dir(self, tmpdir): prefix="base.", rootdir=tmpdir, keep=2, lock_timeout=0 ) assert numdir.check() - assert numdir.basename == "base.%d" % i + assert numdir.basename == f"base.{i}" if i >= 1: assert numdir.new(ext=str(i - 1)).check() if i >= 2: @@ -993,7 +993,7 @@ def test_locked_make_numbered_dir(self, tmpdir): for i in range(10): numdir = local.make_numbered_dir(prefix="base2.", rootdir=tmpdir, keep=2) assert numdir.check() - assert numdir.basename == "base2.%d" % i + assert numdir.basename == f"base2.{i}" for j in range(i): assert numdir.new(ext=str(j)).check() diff --git a/testing/example_scripts/dataclasses/test_compare_dataclasses_with_custom_eq.py b/testing/example_scripts/dataclasses/test_compare_dataclasses_with_custom_eq.py index e25bebf74dd..5ae9a02f99b 100644 --- a/testing/example_scripts/dataclasses/test_compare_dataclasses_with_custom_eq.py +++ b/testing/example_scripts/dataclasses/test_compare_dataclasses_with_custom_eq.py @@ -10,8 +10,8 @@ class SimpleDataObject: field_a: int = field() field_b: str = field() - def __eq__(self, __o: object, /) -> bool: - return super().__eq__(__o) + def __eq__(self, o: object, /) -> bool: + return super().__eq__(o) left = SimpleDataObject(1, "b") right = SimpleDataObject(1, "c") diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index df6dbaee0fd..4e7e441768c 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1438,13 +1438,13 @@ def test_parametrize_scope_overrides( self, pytester: Pytester, scope: str, length: int ) -> None: pytester.makepyfile( - """ + f""" import pytest values = [] def pytest_generate_tests(metafunc): if "arg" in metafunc.fixturenames: metafunc.parametrize("arg", [1,2], indirect=True, - scope=%r) + scope={scope!r}) @pytest.fixture def arg(request): values.append(request.param) @@ -1454,9 +1454,8 @@ def test_hello(arg): def test_world(arg): assert arg in (1,2) def test_checklength(): - assert len(values) == %d + assert len(values) == {length} """ - % (scope, length) ) reprec = pytester.inline_run() reprec.assertoutcome(passed=5) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index a14c4125cf6..a2e2304d342 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1406,15 +1406,14 @@ def test_full_output_truncated(self, monkeypatch, pytester: Pytester) -> None: line_len = 100 expected_truncated_lines = 2 pytester.makepyfile( - r""" + rf""" def test_many_lines(): - a = list([str(i)[0] * %d for i in range(%d)]) + a = list([str(i)[0] * {line_len} for i in range({line_count})]) b = a[::2] a = '\n'.join(map(str, a)) b = '\n'.join(map(str, b)) assert a == b """ - % (line_len, line_count) ) monkeypatch.delenv("CI", raising=False) @@ -1424,7 +1423,7 @@ def test_many_lines(): [ "*+ 1*", "*+ 3*", - "*truncated (%d lines hidden)*use*-vv*" % expected_truncated_lines, + f"*truncated ({expected_truncated_lines} lines hidden)*use*-vv*", ] ) diff --git a/testing/test_conftest.py b/testing/test_conftest.py index bbb1d301ebe..bd083574ffc 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -699,9 +699,9 @@ def out_of_reach(): pass result = pytester.runpytest(*args) match = "" if passed: - match += "*%d passed*" % passed + match += f"*{passed} passed*" if error: - match += "*%d error*" % error + match += f"*{error} error*" result.stdout.fnmatch_lines(match) diff --git a/testing/test_doctest.py b/testing/test_doctest.py index d3ad09da871..1a852ebc82a 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1328,7 +1328,7 @@ def test_bar(): params = ("--doctest-modules",) if enable_doctest else () passes = 3 if enable_doctest else 2 result = pytester.runpytest(*params) - result.stdout.fnmatch_lines(["*=== %d passed in *" % passes]) + result.stdout.fnmatch_lines([f"*=== {passes} passed in *"]) @pytest.mark.parametrize("scope", SCOPES) @pytest.mark.parametrize("autouse", [True, False]) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 872703900cd..6fa04be28b1 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -49,7 +49,7 @@ def __init__(self, verbosity=0): @property def args(self): values = [] - values.append("--verbosity=%d" % self.verbosity) + values.append(f"--verbosity={self.verbosity}") return values