From 5ff50329b59906326ab732d0fcd0218933ae4ed4 Mon Sep 17 00:00:00 2001 From: Sourcery AI <> Date: Wed, 12 Oct 2022 03:04:39 +0000 Subject: [PATCH] 'Refactored by Sourcery' --- doc/en/conf.py | 17 +- doc/en/example/assertion/failure_demo.py | 5 +- .../assertion/test_setup_flow_example.py | 8 +- extra/get_issues.py | 20 +- scripts/prepare-release-pr.py | 3 +- scripts/publish-gh-release-notes.py | 6 +- scripts/update-plugin-list.py | 20 +- src/_pytest/_argcomplete.py | 2 +- src/_pytest/_code/code.py | 112 +++---- src/_pytest/_code/source.py | 30 +- src/_pytest/_io/saferepr.py | 12 +- src/_pytest/_io/terminalwriter.py | 25 +- src/_pytest/_io/wcwidth.py | 5 +- src/_pytest/assertion/__init__.py | 9 +- src/_pytest/assertion/rewrite.py | 93 +++--- src/_pytest/assertion/truncate.py | 2 +- src/_pytest/assertion/util.py | 74 ++--- src/_pytest/cacheprovider.py | 29 +- src/_pytest/capture.py | 39 +-- src/_pytest/compat.py | 16 +- src/_pytest/config/__init__.py | 91 +++-- src/_pytest/config/argparsing.py | 68 ++-- src/_pytest/config/findpaths.py | 38 +-- src/_pytest/debugging.py | 39 +-- src/_pytest/doctest.py | 27 +- src/_pytest/fixtures.py | 119 +++---- src/_pytest/freeze_support.py | 7 +- src/_pytest/helpconfig.py | 31 +- src/_pytest/junitxml.py | 52 ++- src/_pytest/legacypath.py | 15 +- src/_pytest/logging.py | 26 +- src/_pytest/main.py | 134 ++++---- src/_pytest/mark/__init__.py | 21 +- src/_pytest/mark/expression.py | 45 ++- src/_pytest/mark/structures.py | 4 +- src/_pytest/monkeypatch.py | 33 +- src/_pytest/nodes.py | 38 +-- src/_pytest/outcomes.py | 19 +- src/_pytest/pastebin.py | 36 +- src/_pytest/pathlib.py | 42 +-- src/_pytest/pytester.py | 85 +++-- src/_pytest/python.py | 160 +++++---- src/_pytest/python_api.py | 26 +- src/_pytest/recwarn.py | 37 ++- src/_pytest/reports.py | 82 ++--- src/_pytest/runner.py | 25 +- src/_pytest/scope.py | 5 +- src/_pytest/setuponly.py | 7 +- src/_pytest/skipping.py | 24 +- src/_pytest/stepwise.py | 22 +- src/_pytest/terminal.py | 314 +++++++++--------- src/_pytest/unittest.py | 36 +- testing/acceptance_test.py | 17 +- testing/code/test_code.py | 10 +- testing/code/test_excinfo.py | 29 +- testing/code/test_source.py | 6 +- testing/conftest.py | 8 +- testing/deprecated_test.py | 6 +- .../doctest/main_py/__main__.py | 2 +- testing/example_scripts/issue_519.py | 4 +- .../tmpdir/tmp_path_fixture.py | 2 +- testing/freeze/tox_run.py | 3 +- testing/io/test_saferepr.py | 6 +- testing/logging/test_fixture.py | 2 +- testing/logging/test_formatter.py | 2 +- testing/logging/test_reporting.py | 14 +- .../plugins_integration/simple_integration.py | 4 +- testing/python/approx.py | 32 +- testing/python/collect.py | 8 +- testing/python/fixtures.py | 4 +- testing/python/metafunc.py | 4 +- testing/python/raises.py | 4 +- testing/test_argcomplete.py | 4 +- testing/test_assertion.py | 48 ++- testing/test_assertrewrite.py | 32 +- testing/test_cacheprovider.py | 2 +- testing/test_capture.py | 8 +- testing/test_collection.py | 20 +- testing/test_compat.py | 30 +- testing/test_config.py | 30 +- testing/test_conftest.py | 27 +- testing/test_debugging.py | 48 ++- testing/test_doctest.py | 6 +- testing/test_faulthandler.py | 4 +- testing/test_junitxml.py | 80 ++--- testing/test_legacypath.py | 2 +- testing/test_link_resolve.py | 11 +- testing/test_mark.py | 4 +- testing/test_meta.py | 4 +- testing/test_nodes.py | 2 +- testing/test_parseopt.py | 19 +- testing/test_pastebin.py | 2 +- testing/test_pathlib.py | 12 +- testing/test_pluginmanager.py | 2 +- testing/test_pytester.py | 12 +- testing/test_python_path.py | 2 +- testing/test_runner.py | 9 +- testing/test_session.py | 4 +- testing/test_skipping.py | 2 +- testing/test_stash.py | 2 +- testing/test_terminal.py | 40 +-- testing/test_tmpdir.py | 12 +- testing/test_unittest.py | 10 +- testing/test_warnings.py | 20 +- 104 files changed, 1297 insertions(+), 1615 deletions(-) diff --git a/doc/en/conf.py b/doc/en/conf.py index 5184ee7b1e5..63b74888b8e 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -193,7 +193,7 @@ html_title = "pytest documentation" # A shorter title for the navigation bar. Default is the same as html_title. -html_short_title = "pytest-%s" % release +html_short_title = f"pytest-{release}" # The name of an image file (relative to this directory) to place at the top # of the sidebar. @@ -402,17 +402,20 @@ def configure_logging(app: "sphinx.application.Sphinx") -> None: import sphinx.util.logging import logging + + class WarnLogFilter(logging.Filter): def filter(self, record: logging.LogRecord) -> bool: """Ignore warnings about missing include with "only" directive. Ref: https://github.com/sphinx-doc/sphinx/issues/2150.""" - if ( - record.msg.startswith('Problems with "include" directive path:') - and "_changelog_towncrier_draft.rst" in record.msg - ): - return False - return True + return ( + not record.msg.startswith( + 'Problems with "include" directive path:' + ) + or "_changelog_towncrier_draft.rst" not in record.msg + ) + logger = logging.getLogger(sphinx.util.logging.NAMESPACE) warn_handler = [x for x in logger.handlers if x.level == logging.WARNING] diff --git a/doc/en/example/assertion/failure_demo.py b/doc/en/example/assertion/failure_demo.py index abb9bce5097..3cdf7c9a4fc 100644 --- a/doc/en/example/assertion/failure_demo.py +++ b/doc/en/example/assertion/failure_demo.py @@ -77,7 +77,7 @@ def test_eq_longer_list(self): assert [1, 2] == [1, 2, 3] def test_in_list(self): - assert 1 in [0, 2, 3, 4, 5] + assert 1 in {0, 2, 3, 4, 5} def test_not_in_text_multiline(self): text = "some multiline\ntext\nwhich\nincludes foo\nand a\ntail" @@ -180,8 +180,7 @@ def test_reinterpret_fails_with_print_for_the_fun_of_it(self): a, b = items.pop() def test_some_error(self): - if namenotexi: # NOQA - pass + pass def func1(self): assert 41 == 42 diff --git a/doc/en/example/assertion/test_setup_flow_example.py b/doc/en/example/assertion/test_setup_flow_example.py index 0e7eded06b6..1808a6c6b7b 100644 --- a/doc/en/example/assertion/test_setup_flow_example.py +++ b/doc/en/example/assertion/test_setup_flow_example.py @@ -3,11 +3,11 @@ def setup_module(module): class TestStateFullThing: - def setup_class(cls): - cls.classcount += 1 + def setup_class(self): + self.classcount += 1 - def teardown_class(cls): - cls.classcount -= 1 + def teardown_class(self): + self.classcount -= 1 def setup_method(self, method): self.id = eval(method.__name__[5:]) diff --git a/extra/get_issues.py b/extra/get_issues.py index 4aaa3c3ec31..16262ba0e7f 100644 --- a/extra/get_issues.py +++ b/extra/get_issues.py @@ -46,10 +46,10 @@ def main(args): def _get_kind(issue): labels = [label["name"] for label in issue["labels"]] - for key in ("bug", "enhancement", "proposal"): - if key in labels: - return key - return "issue" + return next( + (key for key in ("bug", "enhancement", "proposal") if key in labels), + "issue", + ) def report(issues): @@ -59,15 +59,15 @@ def report(issues): kind = _get_kind(issue) status = issue["state"] number = issue["number"] - link = "https://github.com/pytest-dev/pytest/issues/%s/" % number + link = f"https://github.com/pytest-dev/pytest/issues/{number}/" print("----") print(status, kind, link) print(title) - # print() - # lines = body.split("\n") - # print("\n".join(lines[:3])) - # if len(lines) > 3 or len(body) > 240: - # print("...") + # print() + # lines = body.split("\n") + # print("\n".join(lines[:3])) + # if len(lines) > 3 or len(body) > 240: + # print("...") print("\n\nFound %s open issues" % len(issues)) diff --git a/scripts/prepare-release-pr.py b/scripts/prepare-release-pr.py index 7a80de7edaa..d9d519bc2ae 100644 --- a/scripts/prepare-release-pr.py +++ b/scripts/prepare-release-pr.py @@ -139,8 +139,7 @@ def find_next_version( output = check_output(["git", "tag"], encoding="UTF-8") valid_versions = [] for v in output.splitlines(): - m = re.match(r"\d.\d.\d+$", v.strip()) - if m: + if m := re.match(r"\d.\d.\d+$", v.strip()): valid_versions.append(tuple(int(x) for x in v.split("."))) valid_versions.sort() diff --git a/scripts/publish-gh-release-notes.py b/scripts/publish-gh-release-notes.py index 68cbd7adffd..430c4d9bd9c 100644 --- a/scripts/publish-gh-release-notes.py +++ b/scripts/publish-gh-release-notes.py @@ -43,12 +43,10 @@ def parse_changelog(tag_name): consuming_version = False version_lines = [] for line in changelog_lines: - m = title_regex.match(line) - if m: + if m := title_regex.match(line): # found the version we want: start to consume lines until we find the next version title - if m.group(1) == tag_name: + if m[1] == tag_name: consuming_version = True - # found a new version title while parsing the version we want: break out elif consuming_version: break if consuming_version: diff --git a/scripts/update-plugin-list.py b/scripts/update-plugin-list.py index c034c72420b..e39fc131321 100644 --- a/scripts/update-plugin-list.py +++ b/scripts/update-plugin-list.py @@ -52,11 +52,12 @@ def iter_plugins(): regex = r">([\d\w-]*)" response = requests.get("https://pypi.org/simple") - matches = list( + matches = [ match for match in re.finditer(regex, response.text) if match.groups()[0].startswith("pytest-") - ) + ] + for match in tqdm(matches, smoothing=0): name = match.groups()[0] @@ -69,12 +70,15 @@ def iter_plugins(): info = response.json()["info"] if "Development Status :: 7 - Inactive" in info["classifiers"]: continue - for classifier in DEVELOPMENT_STATUS_CLASSIFIERS: - if classifier in info["classifiers"]: - status = classifier[22:] - break - else: - status = "N/A" + status = next( + ( + classifier[22:] + for classifier in DEVELOPMENT_STATUS_CLASSIFIERS + if classifier in info["classifiers"] + ), + "N/A", + ) + requires = "N/A" if info["requires_dist"]: for requirement in info["requires_dist"]: diff --git a/src/_pytest/_argcomplete.py b/src/_pytest/_argcomplete.py index 120f09ff68f..877161ce192 100644 --- a/src/_pytest/_argcomplete.py +++ b/src/_pytest/_argcomplete.py @@ -87,7 +87,7 @@ def __call__(self, prefix: str, **kwargs: Any) -> List[str]: if "*" not in prefix and "?" not in prefix: # We are on unix, otherwise no bash. if not prefix or prefix[-1] == os.path.sep: - globbed.extend(glob(prefix + ".*")) + globbed.extend(glob(f"{prefix}.*")) prefix += "*" globbed.extend(glob(prefix)) for x in sorted(globbed): diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 97985def1c3..16abd6dd993 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -573,9 +573,7 @@ def traceback(self, value: Traceback) -> None: def __repr__(self) -> str: if self._excinfo is None: return "" - return "<{} {} tblen={}>".format( - self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback) - ) + return f"<{self.__class__.__name__} {saferepr(self._excinfo[1])} tblen={len(self.traceback)}>" def exconly(self, tryshort: bool = False) -> str: """Return the exception as a string. @@ -588,9 +586,8 @@ def exconly(self, tryshort: bool = False) -> str: lines = format_exception_only(self.type, self.value) text = "".join(lines) text = text.rstrip() - if tryshort: - if text.startswith(self._striptext): - text = text[len(self._striptext) :] + if tryshort and text.startswith(self._striptext): + text = text[len(self._striptext) :] return text def errisinstance( @@ -726,9 +723,11 @@ def _getentrysource(self, entry: TracebackEntry) -> Optional["Source"]: def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]: if self.funcargs: - args = [] - for argname, argvalue in entry.frame.getargs(var=True): - args.append((argname, saferepr(argvalue))) + args = [ + (argname, saferepr(argvalue)) + for argname, argvalue in entry.frame.getargs(var=True) + ] + return ReprFuncArgs(args) return None @@ -750,11 +749,9 @@ def get_source( if short: lines.append(space_prefix + source.lines[line_index].strip()) else: - for line in source.lines[:line_index]: - lines.append(space_prefix + line) - lines.append(self.flow_marker + " " + source.lines[line_index]) - for line in source.lines[line_index + 1 :]: - lines.append(space_prefix + line) + lines.extend(space_prefix + line for line in source.lines[:line_index]) + lines.append(f"{self.flow_marker} {source.lines[line_index]}") + lines.extend(space_prefix + line for line in source.lines[line_index + 1 :]) if excinfo is not None: indent = 4 if short else self._getindent(source) lines.extend(self.get_exconly(excinfo, indent=indent, markall=True)) @@ -778,30 +775,27 @@ def get_exconly( return lines def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]: - if self.showlocals: - lines = [] - keys = [loc for loc in locals if loc[0] != "@"] - keys.sort() - for name in keys: - value = locals[name] - if name == "__builtins__": - lines.append("__builtins__ = ") - else: + if not self.showlocals: + return None + lines = [] + keys = [loc for loc in locals if loc[0] != "@"] + keys.sort() + for name in keys: + value = locals[name] + if name == "__builtins__": + lines.append("__builtins__ = ") + else: # This formatting could all be handled by the # _repr() function, which is only reprlib.Repr in # disguise, so is very configurable. - if self.truncate_locals: - str_repr = saferepr(value) - else: - str_repr = safeformat(value) - # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)): - lines.append(f"{name:<10} = {str_repr}") - # else: - # self._line("%-10s =\\" % (name,)) - # # XXX - # pprint.pprint(value, stream=self.excinfowriter) - return ReprLocals(lines) - return None + str_repr = saferepr(value) if self.truncate_locals else safeformat(value) + # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)): + lines.append(f"{name:<10} = {str_repr}") + # else: + # self._line("%-10s =\\" % (name,)) + # # XXX + # pprint.pprint(value, stream=self.excinfowriter) + return ReprLocals(lines) def repr_traceback_entry( self, @@ -818,13 +812,10 @@ def repr_traceback_entry( else: line_index = entry.lineno - entry.getfirstlinesource() short = style == "short" - reprargs = self.repr_args(entry) if not short else None + reprargs = None if short else self.repr_args(entry) s = self.get_source(source, line_index, excinfo, short=short) lines.extend(s) - if short: - message = "in %s" % (entry.name) - else: - message = excinfo and excinfo.typename or "" + message = f"in {entry.name}" if short else excinfo and excinfo.typename or "" entry_path = entry.path path = self._makepath(entry_path) reprfileloc = ReprFileLocation(path, entry.lineno + 1, message) @@ -866,7 +857,7 @@ def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTracebac entries.append(reprentry) return ReprTraceback(entries, None, style=self.style) - for index, entry in enumerate(traceback): + for entry in traceback: einfo = (last == entry) and excinfo or None reprentry = self.repr_traceback_entry(entry, einfo) entries.append(reprentry) @@ -1199,22 +1190,22 @@ class ReprFuncArgs(TerminalRepr): args: Sequence[Tuple[str, object]] def toterminal(self, tw: TerminalWriter) -> None: - if self.args: - linesofar = "" - for name, value in self.args: - ns = f"{name} = {value}" - if len(ns) + len(linesofar) + 2 > tw.fullwidth: - if linesofar: - tw.line(linesofar) - linesofar = ns - else: - if linesofar: - linesofar += ", " + ns - else: - linesofar = ns - if linesofar: - tw.line(linesofar) - tw.line("") + if not self.args: + return + linesofar = "" + for name, value in self.args: + ns = f"{name} = {value}" + if len(ns) + len(linesofar) + 2 > tw.fullwidth: + if linesofar: + tw.line(linesofar) + linesofar = ns + elif linesofar: + linesofar += f", {ns}" + else: + linesofar = ns + if linesofar: + tw.line(linesofar) + tw.line("") def getfslineno(obj: object) -> Tuple[Union[str, Path], int]: @@ -1284,9 +1275,4 @@ def filter_traceback(entry: TracebackEntry) -> bool: p = Path(entry.path) parents = p.parents - if _PLUGGY_DIR in parents: - return False - if _PYTEST_DIR in parents: - return False - - return True + return False if _PLUGGY_DIR in parents else _PYTEST_DIR not in parents diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 208cfb80037..677a663be8c 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -38,9 +38,11 @@ def __init__(self, obj: object = None) -> None: self.lines = deindent(src.split("\n")) def __eq__(self, other: object) -> bool: - if not isinstance(other, Source): - return NotImplemented - return self.lines == other.lines + return ( + self.lines == other.lines + if isinstance(other, Source) + else NotImplemented + ) # Ignore type because of https://github.com/python/mypy/issues/4266. __hash__ = None # type: ignore @@ -56,12 +58,11 @@ def __getitem__(self, key: slice) -> "Source": def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: if isinstance(key, int): return self.lines[key] - else: - if key.step not in (None, 1): - raise IndexError("cannot slice a Source with a step") - newsource = Source() - newsource.lines = self.lines[key.start : key.stop] - return newsource + if key.step not in (None, 1): + raise IndexError("cannot slice a Source with a step") + newsource = Source() + newsource.lines = self.lines[key.start : key.stop] + return newsource def __iter__(self) -> Iterator[str]: return iter(self.lines) @@ -152,21 +153,16 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[i # Before Python 3.8, the lineno of a decorated class or function pointed at the decorator. # Since Python 3.8, the lineno points to the class/def, so need to include the decorators. if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): - for d in x.decorator_list: - values.append(d.lineno - 1) + values.extend(d.lineno - 1 for d in x.decorator_list) values.append(x.lineno - 1) for name in ("finalbody", "orelse"): - val: Optional[List[ast.stmt]] = getattr(x, name, None) - if val: + if val := getattr(x, name, None): # Treat the finally/orelse part as its own statement. values.append(val[0].lineno - 1 - 1) values.sort() insert_index = bisect_right(values, lineno) start = values[insert_index - 1] - if insert_index >= len(values): - end = None - else: - end = values[insert_index] + end = None if insert_index >= len(values) else values[insert_index] return start, end diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index c701872238c..a8095994d1e 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -31,7 +31,7 @@ def _ellipsize(s: str, maxsize: int) -> str: if len(s) > maxsize: i = max(0, (maxsize - 3) // 2) j = max(0, maxsize - 3 - i) - return s[:i] + "..." + s[len(s) - j :] + return f"{s[:i]}...{s[len(s) - j:]}" return s @@ -58,11 +58,7 @@ def __init__(self, maxsize: Optional[int], use_ascii: bool = False) -> None: def repr(self, x: object) -> str: try: - if self.use_ascii: - s = ascii(x) - else: - s = super().repr(x) - + s = ascii(x) if self.use_ascii else super().repr(x) except (KeyboardInterrupt, SystemExit): raise except BaseException as exc: @@ -127,9 +123,7 @@ def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str: when maxsize=None, but that might affect some other code. """ try: - if use_ascii: - return ascii(obj) - return repr(obj) + return ascii(obj) if use_ascii else repr(obj) except Exception as exc: return _format_repr_exception(exc, obj) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 379035d858c..91e364ce951 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -98,9 +98,8 @@ def markup(self, text: str, **markup: bool) -> str: for name in markup: if name not in self._esctable: raise ValueError(f"unknown markup: {name!r}") - if self.hasmarkup: - esc = [self._esctable[name] for name, on in markup.items() if on] - if esc: + if esc := [self._esctable[name] for name, on in markup.items() if on]: + if self.hasmarkup: text = "".join("\x1b[%sm" % cod for cod in esc) + text + "\x1b[0m" return text @@ -182,10 +181,9 @@ def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> No """ if indents and len(indents) != len(lines): raise ValueError( - "indents size ({}) should have same size as lines ({})".format( - len(indents), len(lines) - ) + f"indents size ({len(indents)}) should have same size as lines ({len(lines)})" ) + if not indents: indents = [""] * len(lines) source = "\n".join(lines) @@ -208,7 +206,7 @@ def _highlight(self, source: str) -> str: return source else: try: - highlighted: str = highlight( + return highlight( source, PythonLexer(), TerminalFormatter( @@ -216,18 +214,13 @@ def _highlight(self, source: str) -> str: style=os.getenv("PYTEST_THEME"), ), ) - return highlighted + except pygments.util.ClassNotFound: raise UsageError( - "PYTEST_THEME environment variable had an invalid value: '{}'. " - "Only valid pygment styles are allowed.".format( - os.getenv("PYTEST_THEME") - ) + f"""PYTEST_THEME environment variable had an invalid value: '{os.getenv("PYTEST_THEME")}'. Only valid pygment styles are allowed.""" ) + except pygments.util.OptionError: raise UsageError( - "PYTEST_THEME_MODE environment variable had an invalid value: '{}'. " - "The only allowed values are 'dark' and 'light'.".format( - os.getenv("PYTEST_THEME_MODE") - ) + f"""PYTEST_THEME_MODE environment variable had an invalid value: '{os.getenv("PYTEST_THEME_MODE")}'. The only allowed values are 'dark' and 'light'.""" ) diff --git a/src/_pytest/_io/wcwidth.py b/src/_pytest/_io/wcwidth.py index e5c7bf4d868..8404a7d24d9 100644 --- a/src/_pytest/_io/wcwidth.py +++ b/src/_pytest/_io/wcwidth.py @@ -35,10 +35,7 @@ def wcwidth(c: str) -> int: return 0 # Full/Wide east asian characters. - if unicodedata.east_asian_width(c) in ("F", "W"): - return 2 - - return 1 + return 2 if unicodedata.east_asian_width(c) in ("F", "W") else 1 def wcswidth(s: str) -> int: diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index a46e58136ba..aaeff6a0aa5 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -103,11 +103,7 @@ def undo() -> None: def pytest_collection(session: "Session") -> None: - # This hook is only called when test modules are collected - # so for example not in the managing process of pytest-xdist - # (which does not collect test modules). - assertstate = session.config.stash.get(assertstate_key, None) - if assertstate: + if assertstate := session.config.stash.get(assertstate_key, None): if assertstate.hook is not None: assertstate.hook.set_session(session) @@ -169,8 +165,7 @@ def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None: def pytest_sessionfinish(session: "Session") -> None: - assertstate = session.config.stash.get(assertstate_key, None) - if assertstate: + if assertstate := session.config.stash.get(assertstate_key, None): if assertstate.hook is not None: assertstate.hook.set_session(None) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 63f9dd8f27b..8084fbba18a 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -1,4 +1,5 @@ """Rewrite assertion AST to produce nice error messages.""" + import ast import errno import functools @@ -51,7 +52,7 @@ # 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_TAIL = "." + PYTEST_TAG + PYC_EXT +PYC_TAIL = f".{PYTEST_TAG}{PYC_EXT}" class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader): @@ -91,7 +92,7 @@ def find_spec( state = self.config.stash[assertstate_key] if self._early_rewrite_bailout(name, state): return None - state.trace("find_module called for: %s" % name) + state.trace(f"find_module called for: {name}") # Type ignored because mypy is confused about the `self` binding here. spec = self._find_spec(name, path) # type: ignore @@ -110,14 +111,15 @@ def find_spec( else: fn = spec.origin - if not self._should_rewrite(name, fn, state): - return None - - return importlib.util.spec_from_file_location( - name, - fn, - loader=self, - submodule_search_locations=spec.submodule_search_locations, + return ( + importlib.util.spec_from_file_location( + name, + fn, + loader=self, + submodule_search_locations=spec.submodule_search_locations, + ) + if self._should_rewrite(name, fn, state) + else None ) def create_module( @@ -212,10 +214,9 @@ def _should_rewrite(self, name: str, fn: str, state: "AssertionState") -> bool: state.trace(f"rewriting conftest file: {fn!r}") return True - if self.session is not None: - if self.session.isinitpath(absolutepath(fn)): - state.trace(f"matched test file (was specified on cmdline): {fn!r}") - return True + if self.session is not None and self.session.isinitpath(absolutepath(fn)): + state.trace(f"matched test file (was specified on cmdline): {fn!r}") + return True # modules not passed explicitly on the command line are only # rewritten if they match the naming convention for test files @@ -232,7 +233,7 @@ def _is_marked_for_rewrite(self, name: str, state: "AssertionState") -> bool: return self._marked_for_rewrite_cache[name] except KeyError: for marked in self._must_rewrite: - if name == marked or name.startswith(marked + "."): + if name == marked or name.startswith(f"{marked}."): state.trace(f"matched marked file {name!r} (from {marked!r})") self._marked_for_rewrite_cache[name] = True return True @@ -263,7 +264,7 @@ def _warn_already_imported(self, name: str) -> None: self.config.issue_config_time_warning( PytestAssertRewriteWarning( - "Module already imported so cannot be rewritten: %s" % name + f"Module already imported so cannot be rewritten: {name}" ), stacklevel=5, ) @@ -362,21 +363,21 @@ def _read_pyc( return None # Check for invalid or out of date pyc file. if len(data) != (16): - trace("_read_pyc(%s): invalid pyc (too short)" % source) + trace(f"_read_pyc({source}): invalid pyc (too short)") return None if data[:4] != importlib.util.MAGIC_NUMBER: - trace("_read_pyc(%s): invalid pyc (bad magic number)" % source) + trace(f"_read_pyc({source}): invalid pyc (bad magic number)") return None if data[4:8] != b"\x00\x00\x00\x00": - trace("_read_pyc(%s): invalid pyc (unsupported flags)" % source) + trace(f"_read_pyc({source}): invalid pyc (unsupported flags)") return None mtime_data = data[8:12] if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF: - trace("_read_pyc(%s): out of date" % source) + trace(f"_read_pyc({source}): out of date") return None size_data = data[12:16] if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF: - trace("_read_pyc(%s): invalid pyc (incorrect size)" % source) + trace(f"_read_pyc({source}): invalid pyc (incorrect size)") return None try: co = marshal.load(fp) @@ -384,7 +385,7 @@ def _read_pyc( trace(f"_read_pyc({source}): marshal.load error {e}") return None if not isinstance(co, types.CodeType): - trace("_read_pyc(%s): not a code object" % source) + trace(f"_read_pyc({source}): not a code object") return None return co @@ -418,9 +419,7 @@ def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]: verbosity = config.getoption("verbose") if config is not None else 0 if verbosity >= 2: return None - if verbosity >= 1: - return DEFAULT_REPR_MAX_SIZE * 10 - return DEFAULT_REPR_MAX_SIZE + return DEFAULT_REPR_MAX_SIZE * 10 if verbosity >= 1 else DEFAULT_REPR_MAX_SIZE def _format_assertmsg(obj: object) -> str: @@ -488,7 +487,7 @@ def _call_assertion_pass(lineno: int, orig: str, expl: str) -> None: def _check_if_assertion_pass_impl() -> bool: """Check if any plugins implement the pytest_assertion_pass hook in order not to generate explanation unnecessarily (might be expensive).""" - return True if util._assertion_pass else False + return bool(util._assertion_pass) UNARY_MAP = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"} @@ -674,12 +673,10 @@ def run(self, mod: ast.Module) -> None: return expect_docstring = False elif ( - isinstance(item, ast.ImportFrom) - and item.level == 0 - and item.module == "__future__" + not isinstance(item, ast.ImportFrom) + or item.level != 0 + or item.module != "__future__" ): - pass - else: break pos += 1 # Special case: for a decorated function, set the lineno to that of the @@ -716,7 +713,7 @@ def run(self, mod: ast.Module) -> None: for name, field in ast.iter_fields(node): if isinstance(field, list): new: List[ast.AST] = [] - for i, child in enumerate(field): + for child in field: if isinstance(child, ast.Assert): # Transform assert. new.extend(self.visit(child)) @@ -740,7 +737,7 @@ def is_rewrite_disabled(docstring: str) -> bool: def variable(self) -> str: """Get a new variable.""" # Use a character invalid in python identifiers to avoid clashing. - name = "@py_assert" + str(next(self.variable_counter)) + name = f"@py_assert{str(next(self.variable_counter))}" self.variables.append(name) return name @@ -773,9 +770,9 @@ def explanation_param(self, expr: ast.expr) -> str: and expr are placed in the current format context so that it can be used on the next call to .pop_format_context(). """ - specifier = "py" + str(next(self.variable_counter)) + specifier = f"py{str(next(self.variable_counter))}" self.explanation_specifiers[specifier] = expr - return "%(" + specifier + ")s" + return f"%({specifier})s" def push_format_context(self) -> None: """Create a new formatting context. @@ -804,7 +801,7 @@ def pop_format_context(self, expl_expr: ast.expr) -> ast.Name: keys = [ast.Str(key) for key in current.keys()] format_dict = ast.Dict(keys, list(current.values())) form = ast.BinOp(expl_expr, ast.Mod(), format_dict) - name = "@py_format" + str(next(self.variable_counter)) + name = f"@py_format{str(next(self.variable_counter))}" if self.enable_assertion_pass_hook: self.format_variables.append(name) self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form)) @@ -870,8 +867,7 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]: fmt = self.helper("_format_explanation", err_msg) exc = ast.Call(err_name, [fmt], []) raise_ = ast.Raise(exc, None) - statements_fail = [] - statements_fail.extend(self.expl_stmts) + statements_fail = list(self.expl_stmts) statements_fail.append(raise_) # Passed @@ -912,7 +908,7 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]: explanation = "\n>assert " + explanation else: assertmsg = ast.Str("") - explanation = "assert " + explanation + explanation = f"assert {explanation}" template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation)) msg = self.pop_format_context(template) fmt = self.helper("_format_explanation", msg) @@ -1005,11 +1001,11 @@ def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]: res, expl = self.visit(keyword.value) new_kwargs.append(ast.keyword(keyword.arg, res)) if keyword.arg: - arg_expls.append(keyword.arg + "=" + expl) + arg_expls.append(f"{keyword.arg}={expl}") else: # **args have `arg` keywords with an .arg of None - arg_expls.append("**" + expl) + arg_expls.append(f"**{expl}") - expl = "{}({})".format(func_expl, ", ".join(arg_expls)) + expl = f'{func_expl}({", ".join(arg_expls)})' new_call = ast.Call(new_func, new_args, new_kwargs) res = self.assign(new_call) res_expl = self.explanation_param(self.display(res)) @@ -1020,7 +1016,7 @@ def visit_Starred(self, starred: ast.Starred) -> Tuple[ast.Starred, str]: # A Starred node can appear in a function call. res, expl = self.visit(starred.value) new_starred = ast.Starred(res, starred.ctx) - return new_starred, "*" + expl + return new_starred, f"*{expl}" def visit_Attribute(self, attr: ast.Attribute) -> Tuple[ast.Name, str]: if not isinstance(attr.ctx, ast.Load): @@ -1037,7 +1033,7 @@ def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]: left_res, left_expl = self.visit(comp.left) if isinstance(comp.left, (ast.Compare, ast.BoolOp)): left_expl = f"({left_expl})" - res_variables = [self.variable() for i in range(len(comp.ops))] + res_variables = [self.variable() for _ in range(len(comp.ops))] load_names = [ast.Name(v, ast.Load()) for v in res_variables] store_names = [ast.Name(v, ast.Store()) for v in res_variables] it = zip(range(len(comp.ops)), comp.ops, comp.comparators) @@ -1064,10 +1060,7 @@ def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]: ast.Tuple(expls, ast.Load()), ast.Tuple(results, ast.Load()), ) - if len(comp.ops) > 1: - res: ast.expr = ast.BoolOp(ast.And(), load_names) - else: - res = load_names[0] + res = ast.BoolOp(ast.And(), load_names) if len(comp.ops) > 1 else load_names[0] return res, self.explanation_param(self.pop_format_context(expl_call)) @@ -1078,13 +1071,11 @@ def try_makedirs(cache_dir: Path) -> bool: """ try: os.makedirs(cache_dir, exist_ok=True) - except (FileNotFoundError, NotADirectoryError, FileExistsError): + except (FileNotFoundError, NotADirectoryError, FileExistsError, PermissionError): # One of the path components was not a directory: # - we're in a zip file # - it is a file return False - except PermissionError: - return False except OSError as e: # as of now, EROFS doesn't have an equivalent OSError-subclass if e.errno == errno.EROFS: diff --git a/src/_pytest/assertion/truncate.py b/src/_pytest/assertion/truncate.py index ce148dca095..beba40b2eeb 100644 --- a/src/_pytest/assertion/truncate.py +++ b/src/_pytest/assertion/truncate.py @@ -57,7 +57,7 @@ def _truncate_explanation( truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars) # Add ellipsis to final line - truncated_explanation[-1] = truncated_explanation[-1] + "..." + truncated_explanation[-1] = f"{truncated_explanation[-1]}..." # Append useful message to explanation truncated_line_count = len(input_lines) - len(truncated_explanation) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index fc5dfdbd5ba..bd42b76e694 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -79,10 +79,7 @@ def _format_lines(lines: Sequence[str]) -> List[str]: stackcnt = [0] for line in lines[1:]: if line.startswith("{"): - if stackcnt[-1]: - s = "and " - else: - s = "where " + s = "and " if stackcnt[-1] else "where " stack.append(len(result)) stackcnt[-1] += 1 stackcnt.append(0) @@ -197,16 +194,12 @@ def assertrepr_compare( raise except Exception: explanation = [ - "(pytest_assertion plugin: representation of details failed: {}.".format( - _pytest._code.ExceptionInfo.from_current()._getreprcrash() - ), + f"(pytest_assertion plugin: representation of details failed: {_pytest._code.ExceptionInfo.from_current()._getreprcrash()}.", " Probably an object has a faulty __repr__.)", ] - if not explanation: - return None - return [summary] + explanation + return [summary] + explanation if explanation else None def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]: @@ -262,8 +255,9 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: if i > 42: i -= 10 # Provide some context explanation = [ - "Skipping %s identical leading characters in diff, use -v to show" % i + f"Skipping {i} identical leading characters in diff, use -v to show" ] + left = left[i:] right = right[i:] if len(left) == len(right): @@ -273,9 +267,9 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: if i > 42: i -= 10 # Provide some context explanation += [ - "Skipping {} identical trailing " - "characters in diff, use -v to show".format(i) + f"Skipping {i} identical trailing characters in diff, use -v to show" ] + left = left[:-i] right = right[:-i] keepends = True @@ -296,11 +290,11 @@ def _surrounding_parens_on_own_lines(lines: List[str]) -> None: """Move opening/closing parenthesis/bracket to own lines.""" opening = lines[0][:1] if opening in ["(", "[", "{"]: - lines[0] = " " + lines[0][1:] + lines[0] = f" {lines[0][1:]}" lines[:] = [opening] + lines closing = lines[-1][-1:] if closing in [")", "]", "}"]: - lines[-1] = lines[-1][:-1] + "," + lines[-1] = f"{lines[-1][:-1]}," lines[:] = lines + [closing] @@ -368,8 +362,7 @@ def _compare_eq_sequence( return explanation - len_diff = len_left - len_right - if len_diff: + if len_diff := len_left - len_right: if len_diff > 0: dir_with_more = "Left" extra = saferepr(left[len_right]) @@ -396,12 +389,10 @@ def _compare_eq_set( diff_right = right - left if diff_left: explanation.append("Extra items in the left set:") - for item in diff_left: - explanation.append(saferepr(item)) + explanation.extend(saferepr(item) for item in diff_left) if diff_right: explanation.append("Extra items in the right set:") - for item in diff_right: - explanation.append(saferepr(item)) + explanation.extend(saferepr(item) for item in diff_right) return explanation @@ -412,20 +403,18 @@ def _compare_eq_dict( set_left = set(left) set_right = set(right) common = set_left.intersection(set_right) - same = {k: left[k] for k in common if left[k] == right[k]} - if same and verbose < 2: - explanation += ["Omitting %s identical items, use -vv to show" % len(same)] - elif same: - explanation += ["Common items:"] - explanation += pprint.pformat(same).splitlines() - diff = {k for k in common if left[k] != right[k]} - if diff: + if same := {k: left[k] for k in common if left[k] == right[k]}: + if verbose < 2: + explanation += [f"Omitting {len(same)} identical items, use -vv to show"] + else: + explanation += ["Common items:"] + explanation += pprint.pformat(same).splitlines() + if diff := {k for k in common if left[k] != right[k]}: explanation += ["Differing items:"] for k in diff: - explanation += [saferepr({k: left[k]}) + " != " + saferepr({k: right[k]})] + explanation += [f"{saferepr({k: left[k]})} != {saferepr({k: right[k]})}"] extra_left = set_left - set_right - len_extra_left = len(extra_left) - if len_extra_left: + if len_extra_left := len(extra_left): explanation.append( "Left contains %d more item%s:" % (len_extra_left, "" if len_extra_left == 1 else "s") @@ -434,8 +423,7 @@ def _compare_eq_dict( pprint.pformat({k: left[k] for k in extra_left}).splitlines() ) extra_right = set_right - set_left - len_extra_right = len(extra_right) - if len_extra_right: + if len_extra_right := len(extra_right): explanation.append( "Right contains %d more item%s:" % (len_extra_right, "" if len_extra_right == 1 else "s") @@ -462,7 +450,6 @@ def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]: else: assert False - indent = " " same = [] diff = [] for field in fields_to_check: @@ -474,22 +461,25 @@ def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]: explanation = [] if same or diff: explanation += [""] - if same and verbose < 2: - explanation.append("Omitting %s identical items, use -vv to show" % len(same)) - elif same: - explanation += ["Matching attributes:"] - explanation += pprint.pformat(same).splitlines() + if same: + if verbose < 2: + explanation.append(f"Omitting {len(same)} identical items, use -vv to show") + else: + explanation += ["Matching attributes:"] + explanation += pprint.pformat(same).splitlines() if diff: explanation += ["Differing attributes:"] explanation += pprint.pformat(diff).splitlines() + indent = " " for field in diff: field_left = getattr(left, field) field_right = getattr(right, field) explanation += [ "", - "Drill down into differing attribute %s:" % field, + f"Drill down into differing attribute {field}:", ("%s%s: %r != %r") % (indent, field, field_left, field_right), ] + explanation += [ indent + line for line in _compare_eq_any(field_left, field_right, verbose) @@ -510,7 +500,7 @@ def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]: if line.startswith("- "): continue if line.startswith("+ "): - newdiff.append(" " + line[2:]) + newdiff.append(f" {line[2:]}") else: newdiff.append(line) return newdiff diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 777c1b0b05a..9286105d1c5 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -237,7 +237,7 @@ def pytest_make_collect_report(self, collector: nodes.Collector): # Only filter with known failures. if not self._collected_at_least_one_failure: - if not any(x.nodeid in lastfailed for x in result): + if all(x.nodeid not in lastfailed for x in result): return self.lfplugin.config.pluginmanager.register( LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip" @@ -269,13 +269,16 @@ def pytest_make_collect_report( # Packages are Modules, but _last_failed_paths only contains # test-bearing paths and doesn't try to include the paths of their # packages, so don't filter them. - if isinstance(collector, Module) and not isinstance(collector, Package): - if collector.path not in self.lfplugin._last_failed_paths: - self.lfplugin._skipped_files += 1 - - return CollectReport( - collector.nodeid, "passed", longrepr=None, result=[] - ) + if ( + isinstance(collector, Module) + and not isinstance(collector, Package) + and collector.path not in self.lfplugin._last_failed_paths + ): + self.lfplugin._skipped_files += 1 + + return CollectReport( + collector.nodeid, "passed", longrepr=None, result=[] + ) return None @@ -306,7 +309,7 @@ def get_last_failed_paths(self) -> Set[Path]: def pytest_report_collectionfinish(self) -> Optional[str]: if self.active and self.config.getoption("verbose") >= 0: - return "run-last-failure: %s" % self._report_status + return f"run-last-failure: {self._report_status}" return None def pytest_runtest_logreport(self, report: TestReport) -> None: @@ -544,7 +547,7 @@ def cacheshow(config: Config, session: Session) -> int: assert config.cache is not None tw = TerminalWriter() - tw.line("cachedir: " + str(config.cache._cachedir)) + tw.line(f"cachedir: {str(config.cache._cachedir)}") if not config.cache._cachedir.is_dir(): tw.line("cache is empty") return 0 @@ -561,11 +564,11 @@ def cacheshow(config: Config, session: Session) -> int: key = str(valpath.relative_to(vdir)) val = config.cache.get(key, dummy) if val is dummy: - tw.line("%s contains unreadable content, will be ignored" % key) + tw.line(f"{key} contains unreadable content, will be ignored") else: - tw.line("%s contains:" % key) + tw.line(f"{key} contains:") for line in pformat(val).splitlines(): - tw.line(" " + line) + tw.line(f" {line}") ddir = basedir / Cache._CACHE_PREFIX_DIRS if ddir.is_dir(): diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 6131a46df47..784f7b97c02 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -62,10 +62,8 @@ def _colorama_workaround() -> None: fail in various ways. """ if sys.platform.startswith("win32"): - try: + with contextlib.suppress(ImportError): import colorama # noqa: F401 - except ImportError: - pass def _windowsconsoleio_workaround(stream: TextIO) -> None: @@ -106,11 +104,7 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None: return def _reopen_stdio(f, mode): - if not buffered and mode[0] == "w": - buffering = 0 - else: - buffering = -1 - + buffering = 0 if not buffered and mode[0] == "w" else -1 return io.TextIOWrapper( open(os.dup(f.fileno()), mode, buffering), f.encoding, @@ -264,7 +258,7 @@ def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None: if name == "stdin": tmpfile = DontReadFromInput() else: - tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old) + tmpfile = TeeCaptureIO(self._old) if tee else CaptureIO() self.tmpfile = tmpfile self._state = "initialized" @@ -531,17 +525,21 @@ def index(self, value) -> int: return tuple(self).index(value) def __eq__(self, other: object) -> bool: - if not isinstance(other, (CaptureResult, tuple)): - return NotImplemented - return tuple(self) == tuple(other) + return ( + tuple(self) == tuple(other) + if isinstance(other, (CaptureResult, tuple)) + else NotImplemented + ) def __hash__(self) -> int: return hash(tuple(self)) def __lt__(self, other: object) -> bool: - if not isinstance(other, (CaptureResult, tuple)): - return NotImplemented - return tuple(self) < tuple(other) + return ( + tuple(self) < tuple(other) + if isinstance(other, (CaptureResult, tuple)) + else NotImplemented + ) def __repr__(self) -> str: return f"CaptureResult(out={self.out!r}, err={self.err!r})" @@ -675,7 +673,7 @@ def is_capturing(self) -> Union[str, bool]: if self.is_globally_capturing(): return "global" if self._capture_fixture: - return "fixture %s" % self._capture_fixture.request.fixturename + return f"fixture {self._capture_fixture.request.fixturename}" return False # Global capturing control @@ -724,10 +722,9 @@ def set_fixture(self, capture_fixture: "CaptureFixture[Any]") -> None: current_fixture = self._capture_fixture.request.fixturename requested_fixture = capture_fixture.request.fixturename capture_fixture.request.raiseerror( - "cannot use {} and {} at the same time".format( - requested_fixture, current_fixture - ) + f"cannot use {requested_fixture} and {current_fixture} at the same time" ) + self._capture_fixture = capture_fixture def unset_fixture(self) -> None: @@ -886,9 +883,7 @@ def _resume(self) -> None: def _is_started(self) -> bool: """Whether actively capturing -- not disabled or closed.""" - if self._capture is not None: - return self._capture.is_started() - return False + return self._capture.is_started() if self._capture is not None else False @contextlib.contextmanager def disabled(self) -> Generator[None, None, None]: diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index fab4c31107f..728ca2143ee 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -1,4 +1,5 @@ """Python version compatibility code.""" + import enum import functools import inspect @@ -20,15 +21,7 @@ import attr import py -# fmt: off -# Workaround for https://github.com/sphinx-doc/sphinx/issues/10351. -# If `overload` is imported from `compat` instead of from `typing`, -# Sphinx doesn't recognize it as `overload` and the API docs for -# overloaded functions look good again. But type checkers handle -# it fine. -# fmt: on -if True: - from typing import overload as overload +from typing import overload as overload if TYPE_CHECKING: from typing_extensions import Final @@ -211,10 +204,7 @@ def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]: _non_printable_ascii_translate_table = { i: f"\\x{i:02x}" for i in range(128) if i not in range(32, 127) -} -_non_printable_ascii_translate_table.update( - {ord("\t"): "\\t", ord("\r"): "\\r", ord("\n"): "\\n"} -) +} | {ord("\t"): "\\t", ord("\r"): "\\r", ord("\n"): "\\n"} def _translate_non_printable(s: str) -> str: diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 25f156f8b20..163268cb350 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -116,9 +116,7 @@ def __init__( self.excinfo = excinfo def __str__(self) -> str: - return "{}: {} (from {})".format( - self.excinfo[0].__name__, self.excinfo[1], self.path - ) + return f"{self.excinfo[0].__name__}: {self.excinfo[1]} (from {self.path})" def filter_traceback_for_conftest_import_failure( @@ -337,10 +335,7 @@ def _prepareconfig( def _get_directory(path: Path) -> Path: """Get the directory of a path - itself if already a directory.""" - if path.is_file(): - return path.parent - else: - return path + return path.parent if path.is_file() else path def _get_legacy_hook_marks( @@ -479,12 +474,10 @@ def register( if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS: warnings.warn( PytestConfigWarning( - "{} plugin has been merged into the core, " - "please remove it from your requirements.".format( - name.replace("_", "-") - ) + f'{name.replace("_", "-")} plugin has been merged into the core, please remove it from your requirements.' ) ) + return None ret: Optional[str] = super().register(plugin, name) if ret: @@ -497,9 +490,7 @@ def register( return ret def getplugin(self, name: str): - # Support deprecated naming because plugins (xdist e.g.) use it. - plugin: Optional[_PluggyPlugin] = self.get_plugin(name) - return plugin + return self.get_plugin(name) def hasplugin(self, name: str) -> bool: """Return whether a plugin with the given name is registered.""" @@ -706,7 +697,7 @@ def consider_pluginarg(self, arg: str) -> None: if arg.startswith("no:"): name = arg[3:] if name in essential_plugins: - raise UsageError("plugin %s cannot be disabled" % name) + raise UsageError(f"plugin {name} cannot be disabled") # PR #4304: remove stepwise if cacheprovider is blocked. if name == "cacheprovider": @@ -715,17 +706,19 @@ def consider_pluginarg(self, arg: str) -> None: self.set_blocked(name) if not name.startswith("pytest_"): - self.set_blocked("pytest_" + name) + self.set_blocked(f"pytest_{name}") else: name = arg # Unblock the plugin. None indicates that it has been blocked. # There is no interface with pluggy for this. if self._name2plugin.get(name, -1) is None: del self._name2plugin[name] - if not name.startswith("pytest_"): - if self._name2plugin.get("pytest_" + name, -1) is None: - del self._name2plugin["pytest_" + name] - self.import_plugin(arg, consider_entry_points=True) + if ( + not name.startswith("pytest_") + and self._name2plugin.get(f"pytest_{name}", -1) is None + ): + del self._name2plugin[f"pytest_{name}"] + self.import_plugin(name, consider_entry_points=True) def consider_conftest(self, conftestmodule: types.ModuleType) -> None: """:meta private:""" @@ -762,12 +755,13 @@ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> No if self.is_blocked(modname) or self.get_plugin(modname) is not None: return - importspec = "_pytest." + modname if modname in builtin_plugins else modname + importspec = f"_pytest.{modname}" if modname in builtin_plugins else modname self.rewrite_hook.mark_rewrite(importspec) if consider_entry_points: - loaded = self.load_setuptools_entrypoints("pytest11", name=modname) - if loaded: + if loaded := self.load_setuptools_entrypoints( + "pytest11", name=modname + ): return try: @@ -879,8 +873,7 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]: new_package_files = [] for fn in package_files: parts = fn.split("/") - new_fn = "/".join(parts[1:]) - if new_fn: + if new_fn := "/".join(parts[1:]): new_package_files.append(new_fn) if new_package_files: yield from _iter_rewritable_modules(new_package_files) @@ -1082,10 +1075,7 @@ def notify_exception( excinfo: ExceptionInfo[BaseException], option: Optional[argparse.Namespace] = None, ) -> None: - if option and getattr(option, "fulltrace", False): - style: _TracebackStyle = "long" - else: - style = "native" + style = "long" if option and getattr(option, "fulltrace", False) else "native" excrepr = excinfo.getrepr( funcargs=True, showlocals=getattr(option, "showlocals", False), style=style ) @@ -1116,9 +1106,8 @@ def _processopt(self, opt: "Argument") -> None: for name in opt._short_opts + opt._long_opts: self._opt2dest[name] = opt.dest - if hasattr(opt, "default"): - if not hasattr(self.option, opt.dest): - setattr(self.option, opt.dest, opt.default) + if hasattr(opt, "default") and not hasattr(self.option, opt.dest): + setattr(self.option, opt.dest, opt.default) @hookimpl(trylast=True) def pytest_load_initial_conftests(self, early_config: "Config") -> None: @@ -1269,8 +1258,7 @@ def pytest_collection(self) -> Generator[None, None, None]: def _checkversion(self) -> None: import pytest - minver = self.inicfg.get("minversion", None) - if minver: + if minver := self.inicfg.get("minversion", None): # Imported lazily to improve start-up time. from packaging.version import Version @@ -1321,9 +1309,7 @@ def _validate_plugins(self) -> None: missing_plugins.append(required_plugin) if missing_plugins: - raise UsageError( - "Missing required plugins: {}".format(", ".join(missing_plugins)), - ) + raise UsageError(f'Missing required plugins: {", ".join(missing_plugins)}') def _warn_or_fail_if_strict(self, message: str) -> None: if self.known_args_namespace.strict_config: @@ -1352,19 +1338,18 @@ def parse(self, args: List[str], addopts: bool = True) -> None: args = self._parser.parse_setoption( args, self.option, namespace=self.option ) + if not args and self.invocation_params.dir == self.rootpath: + source = Config.ArgsSource.TESTPATHS + testpaths: List[str] = self.getini("testpaths") + if self.known_args_namespace.pyargs: + args = testpaths + else: + args = [] + for path in testpaths: + args.extend(sorted(glob.iglob(path, recursive=True))) if not args: - if self.invocation_params.dir == self.rootpath: - source = Config.ArgsSource.TESTPATHS - testpaths: List[str] = self.getini("testpaths") - if self.known_args_namespace.pyargs: - args = testpaths - else: - args = [] - for path in testpaths: - args.extend(sorted(glob.iglob(path, recursive=True))) - if not args: - source = Config.ArgsSource.INCOVATION_DIR - args = [str(self.invocation_params.dir)] + source = Config.ArgsSource.INCOVATION_DIR + args = [str(self.invocation_params.dir)] self.args = args self.args_source = source except PrintHelp: @@ -1444,9 +1429,7 @@ def _getini(self, name: str): except KeyError: if default is not None: return default - if type is None: - return "" - return [] + return "" if type is None else [] else: value = override_value # Coerce the values based on types. @@ -1630,9 +1613,9 @@ def _strtobool(val: str) -> bool: .. note:: Copied from distutils.util. """ val = val.lower() - if val in ("y", "yes", "t", "true", "on", "1"): + if val in {"y", "yes", "t", "true", "on", "1"}: return True - elif val in ("n", "no", "f", "false", "off", "0"): + elif val in {"n", "no", "f", "false", "off", "0"}: return False else: raise ValueError(f"invalid truth value {val!r}") diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index d3f01916b61..28f867655cf 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -57,9 +57,8 @@ def __init__( self.extra_info: Dict[str, Any] = {} def processoption(self, option: "Argument") -> None: - if self._processopt: - if option.dest: - self._processopt(option) + if self._processopt and option.dest: + self._processopt(option) def getgroup( self, name: str, description: str = "", after: Optional[str] = None @@ -219,10 +218,7 @@ def __init__(self, msg: str, option: Union["Argument", str]) -> None: self.option_id = str(option) def __str__(self) -> str: - if self.option_id: - return f"option {self.option_id}: {self.msg}" - else: - return self.msg + return f"option {self.option_id}: {self.msg}" if self.option_id else self.msg class Argument: @@ -273,8 +269,7 @@ def __init__(self, *names: str, **attrs: Any) -> None: except KeyError: pass self._set_opt_strings(names) - dest: Optional[str] = attrs.get("dest") - if dest: + if dest := attrs.get("dest"): self.dest = dest elif self._long_opts: self.dest = self._long_opts[0][2:].replace("-", "_") @@ -317,34 +312,34 @@ def _set_opt_strings(self, opts: Sequence[str]) -> None: self, ) elif len(opt) == 2: - if not (opt[0] == "-" and opt[1] != "-"): + if opt[0] != "-" or opt[1] == "-": raise ArgumentError( "invalid short option string %r: " "must be of the form -x, (x any non-dash char)" % opt, self, ) self._short_opts.append(opt) + elif opt[:2] != "--" or opt[2] == "-": + raise ArgumentError( + "invalid long option string %r: " + "must start with --, followed by non-dash" % opt, + self, + ) else: - if not (opt[0:2] == "--" and opt[2] != "-"): - raise ArgumentError( - "invalid long option string %r: " - "must start with --, followed by non-dash" % opt, - self, - ) self._long_opts.append(opt) def __repr__(self) -> str: args: List[str] = [] if self._short_opts: - args += ["_short_opts: " + repr(self._short_opts)] + args += [f"_short_opts: {repr(self._short_opts)}"] if self._long_opts: - args += ["_long_opts: " + repr(self._long_opts)] - args += ["dest: " + repr(self.dest)] + args += [f"_long_opts: {repr(self._long_opts)}"] + args += [f"dest: {repr(self.dest)}"] if hasattr(self, "type"): - args += ["type: " + repr(self.type)] + args += [f"type: {repr(self.type)}"] if hasattr(self, "default"): - args += ["default: " + repr(self.default)] - return "Argument({})".format(", ".join(args)) + args += [f"default: {repr(self.default)}"] + return f'Argument({", ".join(args)})' class OptionGroup: @@ -378,11 +373,10 @@ def addoption(self, *opts: str, **attrs: Any) -> None: Same attributes as the argparse library's :py:func:`add_argument() ` function accepts. """ - conflict = set(opts).intersection( + if conflict := set(opts).intersection( name for opt in self.options for name in opt.names() - ) - if conflict: - raise ValueError("option names %s already added" % conflict) + ): + raise ValueError(f"option names {conflict} already added") option = Argument(*opts, **attrs) self._addoption_instance(option, shortupper=False) @@ -417,7 +411,7 @@ def __init__( ) # extra_info is a dict of (param -> value) to display if there's # an usage error to provide more contextual information to the user. - self.extra_info = extra_info if extra_info else {} + self.extra_info = extra_info or {} def error(self, message: str) -> NoReturn: """Transform argparse error message into UsageError.""" @@ -440,9 +434,8 @@ def parse_args( # type: ignore if unrecognized: for arg in unrecognized: if arg and arg[0] == "-": - lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))] - for k, v in sorted(self.extra_info.items()): - lines.append(f" {k}: {v}") + lines = [f'unrecognized arguments: {" ".join(unrecognized)}'] + lines.extend(f" {k}: {v}" for k, v in sorted(self.extra_info.items())) self.error("\n".join(lines)) getattr(parsed, FILE_OR_DIR).extend(unrecognized) return parsed @@ -455,7 +448,7 @@ def _parse_optional( ) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]: if not arg_string: return None - if not arg_string[0] in self.prefix_chars: + if arg_string[0] not in self.prefix_chars: return None if arg_string in self._option_string_actions: action = self._option_string_actions[arg_string] @@ -478,12 +471,12 @@ def _parse_optional( elif len(option_tuples) == 1: (option_tuple,) = option_tuples return option_tuple - if self._negative_number_matcher.match(arg_string): - if not self._has_negative_number_optionals: - return None - if " " in arg_string: + if ( + self._negative_number_matcher.match(arg_string) + and not self._has_negative_number_optionals + ): return None - return None, arg_string, None + return None if " " in arg_string else (None, arg_string, None) class DropShorterLongHelpFormatter(argparse.HelpFormatter): @@ -504,8 +497,7 @@ def _format_action_invocation(self, action: argparse.Action) -> str: orgstr = super()._format_action_invocation(action) if orgstr and orgstr[0] != "-": # only optional arguments return orgstr - res: Optional[str] = getattr(action, "_formatted_action_invocation", None) - if res: + if res := getattr(action, "_formatted_action_invocation", None): return res options = orgstr.split(", ") if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2): diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 43c2367793e..1da69c4d0ff 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -47,12 +47,10 @@ def load_config_dict_from_file( if "pytest" in iniconfig: return dict(iniconfig["pytest"].items()) - else: - # "pytest.ini" files are always the source of configuration, even if empty. - if filepath.name == "pytest.ini": - return {} + # "pytest.ini" files are always the source of configuration, even if empty. + if filepath.name == "pytest.ini": + return {} - # '.cfg' files are considered if they contain a "[tool:pytest]" section. elif filepath.suffix == ".cfg": iniconfig = _parse_ini_config(filepath) @@ -63,7 +61,6 @@ def load_config_dict_from_file( # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086). fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False) - # '.toml' files are considered if they contain a [tool.pytest.ini_options] table. elif filepath.suffix == ".toml": if sys.version_info >= (3, 11): import tomllib @@ -123,15 +120,14 @@ def get_common_ancestor(paths: Iterable[Path]) -> Path: continue if common_ancestor is None: common_ancestor = path + elif common_ancestor in path.parents or path == common_ancestor: + continue + elif path in common_ancestor.parents: + common_ancestor = path else: - if common_ancestor in path.parents or path == common_ancestor: - continue - elif path in common_ancestor.parents: - common_ancestor = path - else: - shared = commonpath(path, common_ancestor) - if shared is not None: - common_ancestor = shared + shared = commonpath(path, common_ancestor) + if shared is not None: + common_ancestor = shared if common_ancestor is None: common_ancestor = Path.cwd() elif common_ancestor.is_file(): @@ -147,9 +143,7 @@ def get_file_part_from_node_id(x: str) -> str: return x.split("::")[0] def get_dir_from_path(path: Path) -> Path: - if path.is_dir(): - return path - return path.parent + return path if path.is_dir() else path.parent def safe_exists(path: Path) -> bool: # This can throw on paths that contain characters unrepresentable at the OS level, @@ -198,10 +192,7 @@ def determine_setup( if dirs != [ancestor]: rootdir, inipath, inicfg = locate_config(dirs) if rootdir is None: - if config is not None: - cwd = config.invocation_params.dir - else: - cwd = Path.cwd() + cwd = config.invocation_params.dir if config is not None else Path.cwd() rootdir = get_common_ancestor([cwd, ancestor]) is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/" if is_fs_root: @@ -210,9 +201,8 @@ def determine_setup( rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg)) if not rootdir.is_dir(): raise UsageError( - "Directory '{}' not found. Check your '--rootdir' option.".format( - rootdir - ) + f"Directory '{rootdir}' not found. Check your '--rootdir' option." ) + assert rootdir is not None return rootdir, inipath, inicfg or {} diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index b99c3fe2d31..9d4ccb91868 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -104,9 +104,7 @@ class pytestPDB: @classmethod def _is_capturing(cls, capman: Optional["CaptureManager"]) -> Union[str, bool]: - if capman: - return capman.is_capturing() - return False + return capman.is_capturing() if capman else False @classmethod def _import_pdb_cls(cls, capman: Optional["CaptureManager"]): @@ -151,17 +149,16 @@ def _import_pdb_cls(cls, capman: Optional["CaptureManager"]): def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]): import _pytest.config - # Type ignored because mypy doesn't support "dynamic" - # inheritance like this. + + class PytestPdbWrapper(pdb_cls): # type: ignore[valid-type,misc] _pytest_capman = capman _continued = False def do_debug(self, arg): cls._recursive_debug += 1 - ret = super().do_debug(arg) cls._recursive_debug -= 1 - return ret + return super().do_debug(arg) def do_continue(self, arg): ret = super().do_continue(arg) @@ -171,16 +168,11 @@ def do_continue(self, arg): tw.line() capman = self._pytest_capman - capturing = pytestPDB._is_capturing(capman) - if capturing: + if capturing := pytestPDB._is_capturing(capman): if capturing == "global": tw.sep(">", "PDB continue (IO-capturing resumed)") else: - tw.sep( - ">", - "PDB continue (IO-capturing resumed for %s)" - % capturing, - ) + tw.sep(">", f"PDB continue (IO-capturing resumed for {capturing})") assert capman is not None capman.resume() else: @@ -216,11 +208,8 @@ def setup(self, f, tb): breakpoint again. """ ret = super().setup(f, tb) - if not ret and self._continued: - # pdb.setup() returns True if the command wants to exit - # from the interaction: do not suspend capturing then. - if self._pytest_capman: - self._pytest_capman.suspend_global_capture(in_=True) + if not ret and self._continued and self._pytest_capman: + self._pytest_capman.suspend_global_capture(in_=True) return ret def get_stack(self, f, t): @@ -232,6 +221,7 @@ def get_stack(self, f, t): i -= 1 return stack, i + return PytestPdbWrapper @classmethod @@ -260,11 +250,7 @@ def _init_pdb(cls, method, *args, **kwargs): if capturing == "global": tw.sep(">", f"PDB {method} (IO-capturing turned off)") elif capturing: - tw.sep( - ">", - "PDB %s (IO-capturing turned off for %s)" - % (method, capturing), - ) + tw.sep(">", f"PDB {method} (IO-capturing turned off for {capturing})") else: tw.sep(">", f"PDB {method}") @@ -286,8 +272,7 @@ class PdbInvoke: def pytest_exception_interact( self, node: Node, call: "CallInfo[Any]", report: BaseReport ) -> None: - capman = node.config.pluginmanager.getplugin("capturemanager") - if capman: + if capman := node.config.pluginmanager.getplugin("capturemanager"): capman.suspend_global_capture(in_=True) out, err = capman.read_global_capture() sys.stdout.write(out) @@ -350,7 +335,7 @@ def _enter_pdb( ("log", rep.caplog), ): if showcapture in (sectionname, "all") and content: - tw.sep(">", "captured " + sectionname) + tw.sep(">", f"captured {sectionname}") if content[-1:] == "\n": content = content[:-1] tw.line(content) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 771f0890d88..2be7f3728dc 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -131,11 +131,9 @@ def pytest_collect_file( if config.option.doctestmodules and not any( (_is_setup_py(file_path), _is_main_py(file_path)) ): - mod: DoctestModule = DoctestModule.from_parent(parent, path=file_path) - return mod + return DoctestModule.from_parent(parent, path=file_path) elif _is_doctest(config, file_path, parent): - txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=file_path) - return txt + return DoctestTextfile.from_parent(parent, path=file_path) return None @@ -302,8 +300,7 @@ def _disable_output_capturing_for_darwin(self) -> None: """Disable output capturing. Otherwise, stdout is lost to doctest (#985).""" if platform.system() != "Darwin": return - capman = self.config.pluginmanager.getplugin("capturemanager") - if capman: + if capman := self.config.pluginmanager.getplugin("capturemanager"): capman.suspend_global_capture(in_=True) out, err = capman.read_global_capture() sys.stdout.write(out) @@ -334,10 +331,7 @@ def repr_failure( # type: ignore[override] example = failure.example test = failure.test filename = test.filename - if test.lineno is None: - lineno = None - else: - lineno = test.lineno + example.lineno + 1 + lineno = None if test.lineno is None else test.lineno + example.lineno + 1 message = type(failure).__name__ # TODO: ReprFileLocation doesn't expect a None lineno. reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type] @@ -367,7 +361,7 @@ def repr_failure( # type: ignore[override] ).split("\n") else: inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info) - lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)] + lines += [f"UNEXPECTED EXCEPTION: {repr(inner_excinfo.value)}"] lines += [ x.strip("\n") for x in traceback.format_exception(*failure.exc_info) ] @@ -376,7 +370,7 @@ def repr_failure( # type: ignore[override] def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]: assert self.dtest is not None - return self.path, self.dtest.lineno, "[doctest] %s" % self.name + return self.path, self.dtest.lineno, f"[doctest] {self.name}" def _get_flag_lookup() -> Dict[str, int]: @@ -406,11 +400,8 @@ def get_optionflags(parent): def _get_continue_on_failure(config): continue_on_failure = config.getvalue("doctest_continue_on_failure") - if continue_on_failure: - # We need to turn off this if we use pdb since we should stop at - # the first failure. - if config.getvalue("usepdb"): - continue_on_failure = False + if continue_on_failure and config.getvalue("usepdb"): + continue_on_failure = False return continue_on_failure @@ -749,4 +740,4 @@ def add_np(doctest_namespace): For more details: :ref:`doctest_namespace`. """ - return dict() + return {} diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d79895c262b..af5d5d007c1 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -117,14 +117,12 @@ def get_scope_package(node, fixturedef: "FixtureDef[object]"): cls = pytest.Package current = node - fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py") + fixture_package_name = f"{fixturedef.baseid}/__init__.py" while current and ( type(current) is not cls or fixture_package_name != current.nodeid ): current = current.parent - if current is None: - return node.session - return current + return node.session if current is None else current def get_scope_node( @@ -278,8 +276,9 @@ def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]: item_d: Dict[_Key, Deque[nodes.Item]] = defaultdict(deque) items_by_argkey[scope] = item_d for item in items: - keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None) - if keys: + if keys := dict.fromkeys( + get_parametrized_fixture_keys(item, scope), None + ): d[item] = keys for key in keys: item_d[key].append(item) @@ -319,12 +318,14 @@ def reorder_items_atscope( item = items_deque.popleft() if item in items_done or item in no_argkey_group: continue - argkeys = dict.fromkeys( - (k for k in scoped_argkeys_cache.get(item, []) if k not in ignore), None - ) - if not argkeys: - no_argkey_group[item] = None - else: + if argkeys := dict.fromkeys( + ( + k + for k in scoped_argkeys_cache.get(item, []) + if k not in ignore + ), + None, + ): slicing_argkey, _ = argkeys.popitem() # We don't have to remove relevant items from later in the # deque because they'll just be ignored. @@ -335,6 +336,8 @@ def reorder_items_atscope( fix_cache_order(i, argkeys_cache, items_by_argkey) items_deque.appendleft(i) break + else: + no_argkey_group[item] = None if no_argkey_group: no_argkey_group = reorder_items_atscope( no_argkey_group, argkeys_cache, items_by_argkey, scope.next_lower() @@ -472,8 +475,7 @@ def cls(self): """Class (can be None) where the test function was collected.""" if self.scope not in ("class", "function"): raise AttributeError(f"cls not available in {self.scope}-scoped context") - clscol = self._pyfuncitem.getparent(_pytest.python.Class) - if clscol: + if clscol := self._pyfuncitem.getparent(_pytest.python.Class): return clscol.obj @property @@ -628,19 +630,19 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None: param = NOTSET param_index = 0 has_params = fixturedef.params is not None - fixtures_not_supported = getattr(funcitem, "nofuncargs", False) - if has_params and fixtures_not_supported: - msg = ( - "{name} does not support fixtures, maybe unittest.TestCase subclass?\n" - "Node id: {nodeid}\n" - "Function type: {typename}" - ).format( - name=funcitem.name, - nodeid=funcitem.nodeid, - typename=type(funcitem).__name__, - ) - fail(msg, pytrace=False) if has_params: + fixtures_not_supported = getattr(funcitem, "nofuncargs", False) + if fixtures_not_supported: + msg = ( + "{name} does not support fixtures, maybe unittest.TestCase subclass?\n" + "Node id: {nodeid}\n" + "Function type: {typename}" + ).format( + name=funcitem.name, + nodeid=funcitem.nodeid, + typename=type(funcitem).__name__, + ) + fail(msg, pytrace=False) frame = inspect.stack()[3] frameinfo = inspect.getframeinfo(frame[0]) source_path = absolutepath(frameinfo.filename) @@ -651,18 +653,8 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None: ) except ValueError: source_path_str = str(source_path) - msg = ( - "The requested fixture has no parameter defined for test:\n" - " {}\n\n" - "Requested fixture '{}' defined in:\n{}" - "\n\nRequested here:\n{}:{}".format( - funcitem.nodeid, - fixturedef.argname, - getlocation(fixturedef.func, funcitem.config.rootpath), - source_path_str, - source_lineno, - ) - ) + msg = f"The requested fixture has no parameter defined for test:\n {funcitem.nodeid}\n\nRequested fixture '{fixturedef.argname}' defined in:\n{getlocation(fixturedef.func, funcitem.config.rootpath)}\n\nRequested here:\n{source_path_str}:{source_lineno}" + fail(msg, pytrace=False) subrequest = SubRequest( @@ -817,13 +809,12 @@ def formatrepr(self) -> "FixtureLookupErrorRepr": try: lines, _ = inspect.getsourcelines(get_real_func(function)) except (OSError, IndexError, TypeError): - error_msg = "file %s, line %s: source code not available" - addline(error_msg % (fspath, lineno + 1)) + addline(f"file {fspath}, line {lineno + 1}: source code not available") else: addline(f"file {fspath}, line {lineno + 1}") - for i, line in enumerate(lines): + for line in lines: line = line.rstrip() - addline(" " + line) + addline(f" {line}") if line.lstrip().startswith("def"): break @@ -836,12 +827,10 @@ def formatrepr(self) -> "FixtureLookupErrorRepr": if faclist: available.add(name) if self.argname in available: - msg = " recursive dependency involving fixture '{}' detected".format( - self.argname - ) + msg = f" recursive dependency involving fixture '{self.argname}' detected" else: msg = f"fixture '{self.argname}' not found" - msg += "\n available fixtures: {}".format(", ".join(sorted(available))) + msg += f'\n available fixtures: {", ".join(sorted(available))}' msg += "\n use 'pytest --fixtures [testpath]' for help on them." return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname) @@ -866,8 +855,7 @@ def toterminal(self, tw: TerminalWriter) -> None: # tw.line("FixtureLookupError: %s" %(self.argname), red=True) for tbline in self.tblines: tw.line(tbline.rstrip()) - lines = self.errorstring.split("\n") - if lines: + if lines := self.errorstring.split("\n"): tw.line( f"{FormattedExcinfo.fail_marker} {lines[0].strip()}", red=True, @@ -931,11 +919,9 @@ def _eval_scope_callable( result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg] except Exception as e: raise TypeError( - "Error evaluating {} while defining fixture '{}'.\n" - "Expected a function with the signature (*, fixture_name, config)".format( - scope_callable, fixture_name - ) + f"Error evaluating {scope_callable} while defining fixture '{fixture_name}'.\nExpected a function with the signature (*, fixture_name, config)" ) from e + if not isinstance(result, str): fail( "Expected {} to return a 'str' while defining fixture '{}', but it returned:\n" @@ -1075,7 +1061,7 @@ def execute(self, request: SubRequest) -> FixtureValue: return result def cache_key(self, request: SubRequest) -> object: - return request.param_index if not hasattr(request, "param") else request.param + return request.param if hasattr(request, "param") else request.param_index def __repr__(self) -> str: return "".format( @@ -1089,15 +1075,11 @@ def resolve_fixture_function( """Get the actual callable that can be called to obtain the fixture value, dealing with unittest-specific instances and bound methods.""" fixturefunc = fixturedef.func - if fixturedef.unittest: - if request.instance is not None: + if request.instance is not None: + if fixturedef.unittest: # Bind the unbound method to the TestCase instance. fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr] - else: - # The fixture function needs to be bound to the actual - # request.instance so that code working with "fixturedef" behaves - # as expected. - if request.instance is not None: + else: # Handle the case where fixture is defined not in a test class, but some other class # (for example a plugin class with a fixture), see #2270. if hasattr(fixturefunc, "__self__") and not isinstance( @@ -1140,9 +1122,7 @@ def _ensure_immutable_ids( ) -> Optional[Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]]: if ids is None: return None - if callable(ids): - return ids - return tuple(ids) + return ids if callable(ids) else tuple(ids) def _params_converter( @@ -1204,12 +1184,11 @@ def __call__(self, function: FixtureFunction) -> FixtureFunction: if name == "request": location = getlocation(function) fail( - "'request' is a reserved word for fixtures, use another name:\n {}".format( - location - ), + f"'request' is a reserved word for fixtures, use another name:\n {location}", pytrace=False, ) + # Type ignored because https://github.com/python/mypy/issues/2087. function._pytestfixturefunction = self # type: ignore[attr-defined] return function @@ -1315,10 +1294,7 @@ def fixture( # noqa: F811 ) # Direct decoration. - if fixture_function: - return fixture_marker(fixture_function) - - return fixture_marker + return fixture_marker(fixture_function) if fixture_function else fixture_marker def yield_fixture( @@ -1478,8 +1454,7 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: def _getautousenames(self, nodeid: str) -> Iterator[str]: """Return the names of autouse fixtures applicable to nodeid.""" for parentnodeid in nodes.iterparentnodeids(nodeid): - basenames = self._nodeid_autousenames.get(parentnodeid) - if basenames: + if basenames := self._nodeid_autousenames.get(parentnodeid): yield from basenames def getfixtureclosure( diff --git a/src/_pytest/freeze_support.py b/src/_pytest/freeze_support.py index 9f8ea231fed..c9d87ac4350 100644 --- a/src/_pytest/freeze_support.py +++ b/src/_pytest/freeze_support.py @@ -11,8 +11,7 @@ def freeze_includes() -> List[str]: included by cx_freeze.""" import _pytest - result = list(_iter_all_modules(_pytest)) - return result + return list(_iter_all_modules(_pytest)) def _iter_all_modules( @@ -35,10 +34,10 @@ def _iter_all_modules( # Type ignored because typeshed doesn't define ModuleType.__path__ # (only defined on packages). package_path = package.__path__ # type: ignore[attr-defined] - path, prefix = package_path[0], package.__name__ + "." + path, prefix = package_path[0], f"{package.__name__}." for _, name, is_package in pkgutil.iter_modules([path]): if is_package: - for m in _iter_all_modules(os.path.join(path, name), prefix=name + "."): + for m in _iter_all_modules(os.path.join(path, name), prefix=f"{name}."): yield prefix + m else: yield prefix + name diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 151bc6dff95..6ed5f0b2a36 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -132,12 +132,10 @@ def unset_tracing() -> None: def showversion(config: Config) -> None: if config.option.version > 1: sys.stdout.write( - "This is pytest version {}, imported from {}\n".format( - pytest.__version__, pytest.__file__ - ) + f"This is pytest version {pytest.__version__}, imported from {pytest.__file__}\n" ) - plugininfo = getpluginversioninfo(config) - if plugininfo: + + if plugininfo := getpluginversioninfo(config): for line in plugininfo: sys.stdout.write(line + "\n") else: @@ -178,7 +176,7 @@ def showhelp(config: Config) -> None: if help is None: raise TypeError(f"help argument cannot be None for {name}") spec = f"{name} ({type}):" - tw.write(" %s" % spec) + tw.write(f" {spec}") spec_len = len(spec) if spec_len > (indent_len - 3): # Display help starting at a new line. @@ -196,9 +194,9 @@ def showhelp(config: Config) -> None: else: # Display help starting after the spec, following lines indented. tw.write(" " * (indent_len - spec_len - 2)) - wrapped = textwrap.wrap(help, columns - indent_len, break_on_hyphens=False) - - if wrapped: + if wrapped := textwrap.wrap( + help, columns - indent_len, break_on_hyphens=False + ): tw.line(wrapped[0]) for line in wrapped[1:]: tw.line(indent + line) @@ -225,7 +223,7 @@ def showhelp(config: Config) -> None: ) for warningreport in reporter.stats.get("warnings", []): - tw.line("warning : " + warningreport.message, red=True) + tw.line(f"warning : {warningreport.message}", red=True) return @@ -234,13 +232,12 @@ def showhelp(config: Config) -> None: def getpluginversioninfo(config: Config) -> List[str]: lines = [] - plugininfo = config.pluginmanager.list_plugin_distinfo() - if plugininfo: + if plugininfo := config.pluginmanager.list_plugin_distinfo(): lines.append("setuptools registered plugins:") for plugin, dist in plugininfo: loc = getattr(plugin, "__file__", repr(plugin)) content = f"{dist.project_name}-{dist.version} at {loc}" - lines.append(" " + content) + lines.append(f" {content}") return lines @@ -249,17 +246,13 @@ def pytest_report_header(config: Config) -> List[str]: if config.option.debug or config.option.traceconfig: lines.append(f"using: pytest-{pytest.__version__}") - verinfo = getpluginversioninfo(config) - if verinfo: + if verinfo := getpluginversioninfo(config): lines.extend(verinfo) if config.option.traceconfig: lines.append("active plugins:") items = config.pluginmanager.list_name_plugin() for name, plugin in items: - if hasattr(plugin, "__file__"): - r = plugin.__file__ - else: - r = repr(plugin) + r = plugin.__file__ if hasattr(plugin, "__file__") else repr(plugin) lines.append(f" {name:<20}: {r}") return lines diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 7a5170f328b..a9a09a9ca1d 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -50,10 +50,7 @@ def bin_xml_escape(arg: object) -> str: def repl(matchobj: Match[str]) -> str: i = ord(matchobj.group()) - if i <= 0xFF: - return "#x%02X" % i - else: - return "#x%04X" % i + return "#x%02X" % i if i <= 0xFF else "#x%04X" % i # The spec range of valid chars is: # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] @@ -74,9 +71,10 @@ def merge_family(left, right) -> None: left.update(result) -families = {} -families["_base"] = {"testcase": ["classname", "name"]} -families["_base_legacy"] = {"testcase": ["file", "line", "url"]} +families = { + "_base": {"testcase": ["classname", "name"]}, + "_base_legacy": {"testcase": ["file", "line", "url"]}, +} # xUnit 1.x inherits legacy attributes. families["xunit1"] = families["_base"].copy() @@ -102,10 +100,10 @@ def append(self, node: ET.Element) -> None: self.nodes.append(node) def add_property(self, name: str, value: object) -> None: - self.properties.append((str(name), bin_xml_escape(value))) + self.properties.append((name, bin_xml_escape(value))) def add_attribute(self, name: str, value: object) -> None: - self.attrs[str(name)] = bin_xml_escape(value) + self.attrs[name] = bin_xml_escape(value) def make_properties_node(self) -> Optional[ET.Element]: """Return a Junit node containing custom properties, if any.""" @@ -132,7 +130,7 @@ def record_testreport(self, testreport: TestReport) -> None: if hasattr(testreport, "url"): attrs["url"] = testreport.url self.attrs = attrs - self.attrs.update(existing_attrs) # Restore any user-defined attributes. + self.attrs |= existing_attrs # Preserve legacy testcase behavior. if self.family == "xunit1": @@ -140,10 +138,12 @@ def record_testreport(self, testreport: TestReport) -> None: # Filter out attributes not permitted by this test family. # Including custom attributes because they are not valid here. - temp_attrs = {} - for key in self.attrs.keys(): - if key in families[self.family]["testcase"]: - temp_attrs[key] = self.attrs[key] + temp_attrs = { + key: self.attrs[key] + for key in self.attrs + if key in families[self.family]["testcase"] + } + self.attrs = temp_attrs def to_xml(self) -> ET.Element: @@ -202,10 +202,7 @@ def append_failure(self, report: TestReport) -> None: reprcrash: Optional[ReprFileLocation] = getattr( report.longrepr, "reprcrash", None ) - if reprcrash is not None: - message = reprcrash.message - else: - message = str(report.longrepr) + message = reprcrash.message if reprcrash is not None else str(report.longrepr) message = bin_xml_escape(message) self._add_simple("failure", message, str(report.longrepr)) @@ -222,11 +219,7 @@ def append_error(self, report: TestReport) -> None: reprcrash: Optional[ReprFileLocation] = getattr( report.longrepr, "reprcrash", None ) - if reprcrash is not None: - reason = reprcrash.message - else: - reason = str(report.longrepr) - + reason = reprcrash.message if reprcrash is not None else str(report.longrepr) if report.when == "teardown": msg = f'failed on teardown with "{reason}"' else: @@ -444,8 +437,7 @@ def pytest_configure(config: Config) -> None: def pytest_unconfigure(config: Config) -> None: - xml = config.stash.get(xml_key, None) - if xml: + if xml := config.stash.get(xml_key, None): del config.stash[xml_key] config.pluginmanager.unregister(xml) @@ -565,7 +557,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: # The following vars are needed when xdist plugin is used. report_wid = getattr(report, "worker_id", None) report_ii = getattr(report, "item_index", None) - close_report = next( + if close_report := next( ( rep for rep in self.open_reports @@ -576,8 +568,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: ) ), None, - ) - if close_report: + ): # We need to open new testcase in case we have failure in # call and error in teardown in order to follow junit # schema. @@ -605,7 +596,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: self.finalize(report) report_wid = getattr(report, "worker_id", None) report_ii = getattr(report, "item_index", None) - close_report = next( + if close_report := next( ( rep for rep in self.open_reports @@ -616,8 +607,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: ) ), None, - ) - if close_report: + ): self.open_reports.remove(close_report) def update_testcase_duration(self, report: TestReport) -> None: diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index f71e7e96ead..51f699cf3e1 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -101,7 +101,7 @@ def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH: # This ext arguments is likely user error, but since testdir has # allowed this, we will prepend "." as a workaround to avoid breaking # testdir usage that worked before - ext = "." + ext + ext = f".{ext}" return legacy_path(self._pytester.makefile(ext, *args, **kwargs)) def makeconftest(self, source) -> LEGACY_PATH: @@ -396,14 +396,13 @@ def Session_stardir(self: Session) -> LEGACY_PATH: def Config__getini_unknown_type( self, name: str, type: str, value: Union[str, List[str]] ): - if type == "pathlist": - # TODO: This assert is probably not valid in all cases. - assert self.inipath is not None - dp = self.inipath.parent - input_values = shlex.split(value) if isinstance(value, str) else value - return [legacy_path(str(dp / x)) for x in input_values] - else: + if type != "pathlist": raise ValueError(f"unknown configuration type: {type}", value) + # TODO: This assert is probably not valid in all cases. + assert self.inipath is not None + dp = self.inipath.parent + input_values = shlex.split(value) if isinstance(value, str) else value + return [legacy_path(str(dp / x)) for x in input_values] def Node_fspath(self: Node) -> LEGACY_PATH: diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index f9091399f2c..4e7ec8a6f1f 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -161,10 +161,7 @@ def _get_auto_indent(auto_indent_option: Union[int, str, bool, None]) -> int: if auto_indent_option is None: return 0 elif isinstance(auto_indent_option, bool): - if auto_indent_option: - return -1 - else: - return 0 + return -1 if auto_indent_option else 0 elif isinstance(auto_indent_option, int): return int(auto_indent_option) elif isinstance(auto_indent_option, str): @@ -219,8 +216,12 @@ def pytest_addoption(parser: Parser) -> None: def add_option_ini(option, dest, default=None, type=None, **kwargs): parser.addini( - dest, default=default, type=type, help="Default value for " + option + dest, + default=default, + type=type, + help=f"Default value for {option}", ) + group.addoption(option, dest=dest, **kwargs) add_option_ini( @@ -521,9 +522,7 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i except ValueError as e: # Python logging does not recognise this as a logging level raise UsageError( - "'{}' is not recognized as a logging level name for " - "'{}'. Please consider passing the " - "logging level num instead.".format(log_level, setting_name) + f"'{log_level}' is not recognized as a logging level name for '{setting_name}'. Please consider passing the logging level num instead." ) from e @@ -631,8 +630,7 @@ def set_log_path(self, fname: str) -> None: # https://github.com/python/mypy/issues/11193 stream: io.TextIOWrapper = fpath.open(mode="w", encoding="UTF-8") # type: ignore[assignment] - old_stream = self.log_file_handler.setStream(stream) - if old_stream: + if old_stream := self.log_file_handler.setStream(stream): old_stream.close() def _log_cli_enabled(self): @@ -644,11 +642,7 @@ def _log_cli_enabled(self): return False terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter") - if terminal_reporter is None: - # terminal reporter is disabled e.g. by pytest-xdist. - return False - - return True + return terminal_reporter is not None @hookimpl(hookwrapper=True, tryfirst=True) def pytest_sessionstart(self) -> Generator[None, None, None]: @@ -807,7 +801,7 @@ def emit(self, record: logging.LogRecord) -> None: self._test_outcome_written = True self.stream.write("\n") if not self._section_name_shown and self._when: - self.stream.section("live log " + self._when, sep="-", bold=True) + self.stream.section(f"live log {self._when}", sep="-", bold=True) self._section_name_shown = True super().emit(record) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 61fb7eaa4e3..619817df6c6 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -239,9 +239,7 @@ def validate_basetemp(path: str) -> str: def is_ancestor(base: Path, query: Path) -> bool: """Return whether query is an ancestor of base.""" - if base == query: - return True - return query in base.parents + return True if base == query else query in base.parents # check if path is an ancestor of cwd if is_ancestor(Path.cwd(), Path(path).absolute()): @@ -262,39 +260,38 @@ def wrap_session( session.exitstatus = ExitCode.OK initstate = 0 try: + config._do_configure() + initstate = 1 + config.hook.pytest_sessionstart(session=session) + initstate = 2 + session.exitstatus = doit(config, session) or 0 + except UsageError: + session.exitstatus = ExitCode.USAGE_ERROR + raise + except Failed: + session.exitstatus = ExitCode.TESTS_FAILED + except (KeyboardInterrupt, exit.Exception): + excinfo = _pytest._code.ExceptionInfo.from_current() + exitstatus: Union[int, ExitCode] = ExitCode.INTERRUPTED + if isinstance(excinfo.value, exit.Exception): + if excinfo.value.returncode is not None: + exitstatus = excinfo.value.returncode + if initstate < 2: + sys.stderr.write(f"{excinfo.typename}: {excinfo.value.msg}\n") + config.hook.pytest_keyboard_interrupt(excinfo=excinfo) + session.exitstatus = exitstatus + except BaseException: + session.exitstatus = ExitCode.INTERNAL_ERROR + excinfo = _pytest._code.ExceptionInfo.from_current() try: - config._do_configure() - initstate = 1 - config.hook.pytest_sessionstart(session=session) - initstate = 2 - session.exitstatus = doit(config, session) or 0 - except UsageError: - session.exitstatus = ExitCode.USAGE_ERROR - raise - except Failed: - session.exitstatus = ExitCode.TESTS_FAILED - except (KeyboardInterrupt, exit.Exception): - excinfo = _pytest._code.ExceptionInfo.from_current() - exitstatus: Union[int, ExitCode] = ExitCode.INTERRUPTED - if isinstance(excinfo.value, exit.Exception): - if excinfo.value.returncode is not None: - exitstatus = excinfo.value.returncode - if initstate < 2: - sys.stderr.write(f"{excinfo.typename}: {excinfo.value.msg}\n") - config.hook.pytest_keyboard_interrupt(excinfo=excinfo) - session.exitstatus = exitstatus - except BaseException: - session.exitstatus = ExitCode.INTERNAL_ERROR - excinfo = _pytest._code.ExceptionInfo.from_current() - try: - config.notify_exception(excinfo, config.option) - except exit.Exception as exc: - if exc.returncode is not None: - session.exitstatus = exc.returncode - sys.stderr.write(f"{type(exc).__name__}: {exc}\n") - else: - if isinstance(excinfo.value, SystemExit): - sys.stderr.write("mainloop: caught unexpected SystemExit!\n") + config.notify_exception(excinfo, config.option) + except exit.Exception as exc: + if exc.returncode is not None: + session.exitstatus = exc.returncode + sys.stderr.write(f"{type(exc).__name__}: {exc}\n") + else: + if isinstance(excinfo.value, SystemExit): + sys.stderr.write("mainloop: caught unexpected SystemExit!\n") finally: # Explicitly break reference cycle. @@ -379,8 +376,7 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo "collect_ignore", path=collection_path.parent, rootpath=config.rootpath ) ignore_paths = ignore_paths or [] - excludeopt = config.getoption("ignore") - if excludeopt: + if excludeopt := config.getoption("ignore"): ignore_paths.extend(absolutepath(x) for x in excludeopt) if collection_path in ignore_paths: @@ -390,17 +386,14 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo "collect_ignore_glob", path=collection_path.parent, rootpath=config.rootpath ) ignore_globs = ignore_globs or [] - excludeglobopt = config.getoption("ignore_glob") - if excludeglobopt: + if excludeglobopt := config.getoption("ignore_glob"): ignore_globs.extend(absolutepath(x) for x in excludeglobopt) if any(fnmatch.fnmatch(str(collection_path), str(glob)) for glob in ignore_globs): return True allow_in_venv = config.getoption("collect_in_virtualenv") - if not allow_in_venv and _in_venv(collection_path): - return True - return None + return True if not allow_in_venv and _in_venv(collection_path) else None def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> None: @@ -484,8 +477,7 @@ def __init__(self, config: Config) -> None: @classmethod def from_config(cls, config: Config) -> "Session": - session: Session = cls._create(config=config) - return session + return cls._create(config=config) def __repr__(self) -> str: return "<%s %s exitstatus=%r testsfailed=%d testscollected=%d>" % ( @@ -543,16 +535,15 @@ def gethookproxy(self, fspath: "os.PathLike[str]"): self.config.getoption("importmode"), rootpath=self.config.rootpath, ) - remove_mods = pm._conftest_plugins.difference(my_conftestmodules) - if remove_mods: - # One or more conftests are not in use at this fspath. - from .config.compat import PathAwareHookProxy - - proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods)) - else: + if not ( + remove_mods := pm._conftest_plugins.difference(my_conftestmodules) + ): # All plugins are active for this fspath. - proxy = self.config.hook - return proxy + return self.config.hook + # One or more conftests are not in use at this fspath. + from .config.compat import PathAwareHookProxy + + return PathAwareHookProxy(FSHookProxy(pm, remove_mods)) def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if direntry.name == "__pycache__": @@ -562,9 +553,7 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): return False norecursepatterns = self.config.getini("norecursedirs") - if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns): - return False - return True + return not any(fnmatch_ex(pat, fspath) for pat in norecursepatterns) def _collectfile( self, fspath: Path, handle_dupes: bool = True @@ -575,9 +564,10 @@ def _collectfile( fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink() ) ihook = self.gethookproxy(fspath) - if not self.isinitpath(fspath): - if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): - return () + if not self.isinitpath(fspath) and ihook.pytest_ignore_collect( + collection_path=fspath, config=self.config + ): + return () if handle_dupes: keepduplicates = self.config.getoption("keepduplicates") @@ -658,10 +648,9 @@ def perform_collect( # noqa: F811 raise UsageError(*errors) if not genitems: items = rep.result - else: - if rep.passed: - for node in rep.result: - self.items.extend(self.genitems(node)) + elif rep.passed: + for node in rep.result: + self.items.extend(self.genitems(node)) self.config.pluginmanager.check_pending() hook.pytest_collection_modifyitems( @@ -703,8 +692,9 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: if parent.is_dir(): pkginit = parent / "__init__.py" if pkginit.is_file() and pkginit not in node_cache1: - col = self._collectfile(pkginit, handle_dupes=False) - if col: + if col := self._collectfile( + pkginit, handle_dupes=False + ): if isinstance(col[0], Package): pkg_roots[str(parent)] = col[0] node_cache1[col[0].path] = [col[0]] @@ -775,16 +765,14 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: rep = collect_one_node(node) matchnodes_cache[key] = rep if rep.passed: - submatchnodes = [] - for r in rep.result: - # TODO: Remove parametrized workaround once collection structure contains - # parametrization. + if submatchnodes := [ + r + for r in rep.result if ( r.name == matchnames[0] or r.name.split("[")[0] == matchnames[0] - ): - submatchnodes.append(r) - if submatchnodes: + ) + ]: work.append((submatchnodes, matchnames[1:])) else: # Report collection failures here to avoid failing to run some test @@ -877,7 +865,7 @@ def resolve_collection_argument( If the path doesn't exist, raise UsageError. If the path is a directory and selection parts are present, raise UsageError. """ - base, squacket, rest = str(arg).partition("[") + base, squacket, rest = arg.partition("[") strpath, *parts = base.split("::") if parts: parts[-1] = f"{parts[-1]}{squacket}{rest}" diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 6717d1135ee..94437d046b4 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -121,7 +121,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: parts = line.split(":", 1) name = parts[0] rest = parts[1] if len(parts) == 2 else "" - tw.write("@pytest.mark.%s:" % name, bold=True) + tw.write(f"@pytest.mark.{name}:", bold=True) tw.line(rest) tw.line() config._ensure_unconfigure() @@ -149,21 +149,19 @@ class KeywordMatcher: @classmethod def from_item(cls, item: "Item") -> "KeywordMatcher": - mapped_names = set() - # Add the names of the current item and any parent items. import pytest - for node in item.listchain(): - if not isinstance(node, pytest.Session): - mapped_names.add(node.name) + mapped_names = { + node.name + for node in item.listchain() + if not isinstance(node, pytest.Session) + } # Add the names added as extra keywords to current or parent items. mapped_names.update(item.listextrakeywords()) - # Add the names attached to the current function through direct assignment. - function_obj = getattr(item, "function", None) - if function_obj: + if function_obj := getattr(item, "function", None): mapped_names.update(function_obj.__dict__) # Add the markers to the keywords as we no longer handle them correctly. @@ -175,10 +173,7 @@ def __call__(self, subname: str) -> bool: subname = subname.lower() names = (name.lower() for name in self._names) - for name in names: - if subname in name: - return True - return False + return any(subname in name for name in names) def deselect_by_keyword(items: "List[Item]", config: Config) -> None: diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index 0a2e7c65676..deac1399deb 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -84,24 +84,22 @@ def lex(self, input: str) -> Iterator[Token]: elif input[pos] == ")": yield Token(TokenType.RPAREN, ")", pos) pos += 1 - else: - match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:]) - if match: - value = match.group(0) - if value == "or": - yield Token(TokenType.OR, value, pos) - elif value == "and": - yield Token(TokenType.AND, value, pos) - elif value == "not": - yield Token(TokenType.NOT, value, pos) - else: - yield Token(TokenType.IDENT, value, pos) - pos += len(value) + elif match := re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:]): + value = match[0] + if value == "or": + yield Token(TokenType.OR, value, pos) + elif value == "and": + yield Token(TokenType.AND, value, pos) + elif value == "not": + yield Token(TokenType.NOT, value, pos) else: - raise ParseError( - pos + 1, - f'unexpected character "{input[pos]}"', - ) + yield Token(TokenType.IDENT, value, pos) + pos += len(value) + else: + raise ParseError( + pos + 1, + f'unexpected character "{input[pos]}"', + ) yield Token(TokenType.EOF, "", pos) def accept(self, type: TokenType, *, reject: bool = False) -> Optional[Token]: @@ -117,10 +115,7 @@ def accept(self, type: TokenType, *, reject: bool = False) -> Optional[Token]: def reject(self, expected: Sequence[TokenType]) -> NoReturn: raise ParseError( self.current.pos + 1, - "expected {}; got {}".format( - " OR ".join(type.value for type in expected), - self.current.type.value, - ), + f'expected {" OR ".join(type.value for type in expected)}; got {self.current.type.value}', ) @@ -162,8 +157,7 @@ def not_expr(s: Scanner) -> ast.expr: ret = expr(s) s.accept(TokenType.RPAREN, reject=True) return ret - ident = s.accept(TokenType.IDENT) - if ident: + if ident := s.accept(TokenType.IDENT): return ast.Name(IDENT_PREFIX + ident.value, ast.Load()) s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT)) @@ -196,7 +190,7 @@ def __init__(self, code: types.CodeType) -> None: self.code = code @classmethod - def compile(self, input: str) -> "Expression": + def compile(cls, input: str) -> "Expression": """Compile a match expression. :param input: The input expression - one line. @@ -218,5 +212,4 @@ def evaluate(self, matcher: Callable[[str], bool]) -> bool: :returns: Whether the expression matches or not. """ - ret: bool = eval(self.code, {"__builtins__": {}}, MatcherAdapter(matcher)) - return ret + return eval(self.code, {"__builtins__": {}}, MatcherAdapter(matcher)) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 800a25c9243..d6b64577ac6 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -514,7 +514,7 @@ def __getattr__(self, name: str) -> MarkDecorator: ) # Raise a specific error for common misspellings of "parametrize". - if name in ["parameterize", "parametrise", "parameterise"]: + if name in {"parameterize", "parametrise", "parameterise"}: __tracebackhide__ = True fail(f"Unknown '{name}' mark, did you mean 'parametrize'?") @@ -584,7 +584,7 @@ def __iter__(self) -> Iterator[str]: def __len__(self) -> int: # Doesn't need to be fast. - return sum(1 for keyword in self) + return sum(1 for _ in self) def __repr__(self) -> str: return f"" diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index c6e29ac7642..b46e476c319 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -62,7 +62,7 @@ def resolve(name: str) -> object: used = parts.pop(0) found: object = __import__(used) for part in parts: - used += "." + part + used += f".{part}" try: found = getattr(found, part) except AttributeError: @@ -234,13 +234,12 @@ def setattr( ) value = name name, target = derive_importpath(target, raising) - else: - if not isinstance(name, str): - raise TypeError( - "use setattr(target, name, value) with name being a string or " - "setattr(target, value) with target being a dotted " - "import string" - ) + elif not isinstance(name, str): + raise TypeError( + "use setattr(target, name, value) with name being a string or " + "setattr(target, value) with target being a dotted " + "import string" + ) oldval = getattr(target, name, notset) if raising and oldval is notset: @@ -279,10 +278,7 @@ def delattr( ) name, target = derive_importpath(target, raising) - if not hasattr(target, name): - if raising: - raise AttributeError(name) - else: + if hasattr(target, name): oldval = getattr(target, name, notset) # Avoid class descriptors like staticmethod/classmethod. if inspect.isclass(target): @@ -290,6 +286,9 @@ def delattr( self._setattr.append((target, name, oldval)) delattr(target, name) + elif raising: + raise AttributeError(name) + def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None: """Set dictionary entry ``name`` to value.""" self._setitem.append((dic, name, dic.get(name, notset))) @@ -301,13 +300,13 @@ def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> N Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to False. """ - if name not in dic: - if raising: - raise KeyError(name) - else: + if name in dic: self._setitem.append((dic, name, dic.get(name, notset))) del dic[name] + elif raising: + raise KeyError(name) + def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None: """Set environment variable ``name`` to ``value``. @@ -325,7 +324,7 @@ def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None: ), stacklevel=2, ) - value = str(value) + value = value if prepend and name in os.environ: value = value + prepend + os.environ[name] self.setitem(os.environ, name, value) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index cfb9b5a3634..7fd95ce3019 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -201,19 +201,19 @@ def __init__( if config: #: The pytest config object. self.config: Config = config - else: - if not parent: - raise TypeError("config or parent must be provided") + elif parent: self.config = parent.config + else: + raise TypeError("config or parent must be provided") if session: #: The pytest session this node is part of. self.session: Session = session - else: - if not parent: - raise TypeError("session or parent must be provided") + elif parent: self.session = parent.session + else: + raise TypeError("session or parent must be provided") if path is None and fspath is None: path = getattr(parent, "path", None) #: Filesystem path where this node was collected from (can be None). @@ -232,11 +232,11 @@ def __init__( if nodeid is not None: assert "::()" not in nodeid self._nodeid = nodeid - else: - if not self.parent: - raise TypeError("nodeid or parent must be provided") - self._nodeid = self.parent.nodeid + "::" + self.name + elif self.parent: + self._nodeid = f"{self.parent.nodeid}::{self.name}" + else: + raise TypeError("nodeid or parent must be provided") #: A place where plugins can store information on the node for their #: own use. self.stash: Stash = Stash() @@ -267,7 +267,7 @@ def ihook(self): return self.session.gethookproxy(self.path) def __repr__(self) -> str: - return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None)) + return f'<{self.__class__.__name__} {getattr(self, "name", None)}>' def warn(self, warning: Warning) -> None: """Issue a warning for this Node. @@ -444,9 +444,8 @@ def _repr_failure_py( if isinstance(excinfo.value, ConftestImportFailure): excinfo = ExceptionInfo.from_exc_info(excinfo.value.excinfo) - if isinstance(excinfo.value, fail.Exception): - if not excinfo.value.pytrace: - style = "value" + if isinstance(excinfo.value, fail.Exception) and not excinfo.value.pytrace: + style = "value" if isinstance(excinfo.value, FixtureLookupError): return excinfo.value.formatrepr() if self.config.getoption("fulltrace", False): @@ -465,11 +464,7 @@ def _repr_failure_py( else: style = "long" - if self.config.getoption("verbose", 0) > 1: - truncate_locals = False - else: - truncate_locals = True - + truncate_locals = self.config.getoption("verbose", 0) <= 1 # excinfo.getrepr() formats paths relative to the CWD if `abspath` is False. # It is possible for a fixture/test to change the CWD while this code runs, which # would then result in the user seeing confusing paths in the failure message. @@ -710,10 +705,9 @@ def _check_item_and_collector_diamond_inheritance(self) -> None: return setattr(cls, attr_name, True) - problems = ", ".join( + if problems := ", ".join( base.__name__ for base in cls.__bases__ if issubclass(base, Collector) - ) - if problems: + ): warnings.warn( f"{cls.__name__} is an Item subclass and should not be a collector, " f"however its bases {problems} are collectors.\n" diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index e46b663dd50..16f8dd409b3 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -295,14 +295,13 @@ def importorskip( if minversion is None: return mod verattr = getattr(mod, "__version__", None) - if minversion is not None: - # Imported lazily to improve start-up time. - from packaging.version import Version - - if verattr is None or Version(verattr) < Version(minversion): - raise Skipped( - "module %r has __version__ %r, required is: %r" - % (modname, verattr, minversion), - allow_module_level=True, - ) + # Imported lazily to improve start-up time. + from packaging.version import Version + + if verattr is None or Version(verattr) < Version(minversion): + raise Skipped( + "module %r has __version__ %r, required is: %r" + % (modname, verattr, minversion), + allow_module_level=True, + ) return mod diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index 22c7a622373..ea0ec8d2e7b 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -30,23 +30,24 @@ def pytest_addoption(parser: Parser) -> None: @pytest.hookimpl(trylast=True) def pytest_configure(config: Config) -> None: - if config.option.pastebin == "all": - tr = config.pluginmanager.getplugin("terminalreporter") - # If no terminal reporter plugin is present, nothing we can do here; - # this can happen when this function executes in a worker node - # when using pytest-xdist, for example. - if tr is not None: - # pastebin file will be UTF-8 encoded binary file. - config.stash[pastebinfile_key] = tempfile.TemporaryFile("w+b") - oldwrite = tr._tw.write + if config.option.pastebin != "all": + return + tr = config.pluginmanager.getplugin("terminalreporter") + # If no terminal reporter plugin is present, nothing we can do here; + # this can happen when this function executes in a worker node + # when using pytest-xdist, for example. + if tr is not None: + # pastebin file will be UTF-8 encoded binary file. + config.stash[pastebinfile_key] = tempfile.TemporaryFile("w+b") + oldwrite = tr._tw.write - def tee_write(s, **kwargs): - oldwrite(s, **kwargs) - if isinstance(s, str): - s = s.encode("utf-8") - config.stash[pastebinfile_key].write(s) + def tee_write(s, **kwargs): + oldwrite(s, **kwargs) + if isinstance(s, str): + s = s.encode("utf-8") + config.stash[pastebinfile_key].write(s) - tr._tw.write = tee_write + tr._tw.write = tee_write def pytest_unconfigure(config: Config) -> None: @@ -83,9 +84,8 @@ def create_new_paste(contents: Union[str, bytes]) -> str: urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8") ) except OSError as exc_info: # urllib errors - return "bad response: %s" % exc_info - m = re.search(r'href="/raw/(\w+)"', response) - if m: + return f"bad response: {exc_info}" + if m := re.search(r'href="/raw/(\w+)"', response): return f"{url}/show/{m.group(1)}" else: return "bad response: invalid format ('" + response + "')" diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index c5a411b5963..5147157b08d 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -108,7 +108,7 @@ def chmod_rw(p: str) -> None: # Stop when we reach the original path passed to rm_rf. if parent == start_path: break - chmod_rw(str(path)) + chmod_rw(path) func(path) return True @@ -196,19 +196,15 @@ def _force_symlink( the inaccuracy is going to be acceptable. """ current_symlink = root.joinpath(target) - try: + with contextlib.suppress(OSError): current_symlink.unlink() - except OSError: - pass - try: + with contextlib.suppress(Exception): current_symlink.symlink_to(link_to) - except Exception: - pass def make_numbered_dir(root: Path, prefix: str, mode: int = 0o700) -> Path: """Create a directory with an increased number as suffix for the given prefix.""" - for i in range(10): + for _ in range(10): # try up to 10 times to create the folder max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1) new_number = max_existing + 1 @@ -218,13 +214,12 @@ def make_numbered_dir(root: Path, prefix: str, mode: int = 0o700) -> Path: except Exception: pass else: - _force_symlink(root, prefix + "current", new_path) + _force_symlink(root, f"{prefix}current", new_path) return new_path - else: - raise OSError( - "could not create numbered dir with prefix " - "{prefix} in {root} after 10 tries".format(prefix=prefix, root=root) - ) + raise OSError( + "could not create numbered dir with prefix " + "{prefix} in {root} after 10 tries".format(prefix=prefix, root=root) + ) def create_cleanup_lock(p: Path) -> Path: @@ -253,10 +248,8 @@ def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> Non if current_pid != original_pid: # fork return - try: + with contextlib.suppress(OSError): lock_path.unlink() - except OSError: - pass return register(cleanup_on_exit) @@ -283,10 +276,8 @@ def maybe_delete_a_numbered_dir(path: Path) -> None: # If we created the lock, ensure we remove it even if we failed # to properly remove the numbered dir. if lock_path is not None: - try: + with contextlib.suppress(OSError): lock_path.unlink() - except OSError: - pass def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool: @@ -354,7 +345,7 @@ def make_numbered_dir_with_cleanup( ) -> Path: """Create a numbered dir with a cleanup lock and remove old ones.""" e = None - for i in range(10): + for _ in range(10): try: p = make_numbered_dir(root, prefix, mode) lock_path = create_cleanup_lock(p) @@ -379,10 +370,7 @@ def make_numbered_dir_with_cleanup( def resolve_from_str(input: str, rootpath: Path) -> Path: input = expanduser(input) input = expandvars(input) - if isabs(input): - return Path(input) - else: - return rootpath.joinpath(input) + return Path(input) if isabs(input) else rootpath.joinpath(input) def fnmatch_ex(pattern: str, path: Union[str, "os.PathLike[str]"]) -> bool: @@ -544,8 +532,8 @@ def import_path( if module_file.endswith((".pyc", ".pyo")): module_file = module_file[:-1] - if module_file.endswith(os.path.sep + "__init__.py"): - module_file = module_file[: -(len(os.path.sep + "__init__.py"))] + if module_file.endswith(f"{os.path.sep}__init__.py"): + module_file = module_file[:-len(f"{os.path.sep}__init__.py")] try: is_same = _is_same(str(path), module_file) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index a9299944dec..5f3fdc82b8d 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -170,19 +170,19 @@ def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: lines2 = self.get_open_files() new_fds = {t[0] for t in lines2} - {t[0] for t in lines1} - leaked_files = [t for t in lines2 if t[0] in new_fds] - if leaked_files: + if leaked_files := [t for t in lines2 if t[0] in new_fds]: error = [ - "***** %s FD leakage detected" % len(leaked_files), + f"***** {len(leaked_files)} FD leakage detected", *(str(f) for f in leaked_files), "*** Before:", *(str(f) for f in lines1), "*** After:", *(str(f) for f in lines2), - "***** %s FD leakage detected" % len(leaked_files), + f"***** {len(leaked_files)} FD leakage detected", "*** function %s:%s: %s " % item.location, "See issue #2366", ] + item.warn(PytestWarning("\n".join(error))) @@ -304,8 +304,7 @@ def popcall(self, name: str) -> RecordedHookCall: if call._name == name: del self.calls[i] return call - lines = [f"could not find call {name!r}, in:"] - lines.extend([" %s" % x for x in self.calls]) + lines = [f"could not find call {name!r}, in:", *[f" {x}" for x in self.calls]] fail("\n".join(lines)) def getcall(self, name: str) -> RecordedHookCall: @@ -684,10 +683,7 @@ def __init__( self._mod_collections: WeakKeyDictionary[ Collector, List[Union[Item, Collector]] ] = WeakKeyDictionary() - if request.function: - name: str = request.function.__name__ - else: - name = request.node.name + name = request.function.__name__ if request.function else request.node.name self._name = name self._path: Path = tmp_path_factory.mktemp(name, numbered=True) #: A list of plugins to use with :py:meth:`parseconfig` and @@ -957,7 +953,7 @@ def copy_example(self, name: Optional[str] = None) -> Path: if name is None: func_name = self._name maybe_dir = example_dir / func_name - maybe_file = example_dir / (func_name + ".py") + maybe_file = example_dir / f"{func_name}.py" if maybe_dir.is_dir(): example_path = maybe_dir @@ -1112,12 +1108,12 @@ def inline_run( plugins = list(plugins) finalizers = [] try: - # Any sys.module or sys.path changes done while running pytest - # inline should be reverted after the test run completes to avoid - # clashing with later inline tests run within the same pytest test, - # e.g. just because they use matching test module names. - finalizers.append(self.__take_sys_modules_snapshot().restore) - finalizers.append(SysPathsSnapshot().restore) + finalizers.extend( + ( + self.__take_sys_modules_snapshot().restore, + SysPathsSnapshot().restore, + ) + ) # Important note: # - our tests should not leave any other references/registrations @@ -1158,31 +1154,25 @@ def runpytest_inprocess( ) -> RunResult: """Return result of running pytest in-process, providing a similar interface to what self.runpytest() provides.""" - syspathinsert = kwargs.pop("syspathinsert", False) - - if syspathinsert: + if syspathinsert := kwargs.pop("syspathinsert", False): self.syspathinsert() now = timing.time() capture = _get_multicapture("sys") capture.start_capturing() try: - try: - reprec = self.inline_run(*args, **kwargs) - except SystemExit as e: - ret = e.args[0] - try: - ret = ExitCode(e.args[0]) - except ValueError: - pass + reprec = self.inline_run(*args, **kwargs) + except SystemExit as e: + ret = e.args[0] + with contextlib.suppress(ValueError): + ret = ExitCode(e.args[0]) + class reprec: # type: ignore + ret = ret - class reprec: # type: ignore - ret = ret - - except Exception: - traceback.print_exc() + except Exception: + traceback.print_exc() - class reprec: # type: ignore - ret = ExitCode(3) + class reprec: # type: ignore + ret = ExitCode(3) finally: out, err = capture.readouterr() @@ -1217,7 +1207,7 @@ def _ensure_basetemp( if str(x).startswith("--basetemp"): break else: - new_args.append("--basetemp=%s" % self.path.parent.joinpath("basetemp")) + new_args.append(f'--basetemp={self.path.parent.joinpath("basetemp")}') return new_args def parseconfig(self, *args: Union[str, "os.PathLike[str]"]) -> Config: @@ -1335,10 +1325,14 @@ def collect_by_name( """ if modcol not in self._mod_collections: self._mod_collections[modcol] = list(modcol.collect()) - for colitem in self._mod_collections[modcol]: - if colitem.name == name: - return colitem - return None + return next( + ( + colitem + for colitem in self._mod_collections[modcol] + if colitem.name == name + ), + None, + ) def popen( self, @@ -1361,9 +1355,7 @@ def popen( ) kw["env"] = env - if stdin is self.CLOSE_STDIN: - kw["stdin"] = subprocess.PIPE - elif isinstance(stdin, bytes): + if stdin is self.CLOSE_STDIN or isinstance(stdin, bytes): kw["stdin"] = subprocess.PIPE else: kw["stdin"] = stdin @@ -1503,9 +1495,8 @@ def runpytest_subprocess( """ __tracebackhide__ = True p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700) - args = ("--basetemp=%s" % p,) + args - plugins = [x for x in self.plugins if isinstance(x, str)] - if plugins: + args = (f"--basetemp={p}", ) + args + if plugins := [x for x in self.plugins if isinstance(x, str)]: args = ("-p", plugins[0]) + args args = self._getpytestargs() + args return self.run(*args, timeout=timeout) @@ -1708,7 +1699,7 @@ def _match_lines( started = True break elif match_func(nextline, line): - self._log("%s:" % match_nickname, repr(line)) + self._log(f"{match_nickname}:", repr(line)) self._log( "{:>{width}}".format("with:", width=wnick), repr(nextline) ) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 1e30d42ce9c..892e11100e6 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -172,10 +172,11 @@ def pytest_configure(config: Config) -> None: def async_warn_and_skip(nodeid: str) -> None: - msg = "async def functions are not natively supported and have been skipped.\n" - msg += ( - "You need to install a suitable plugin for your async framework, for example:\n" + msg = ( + "async def functions are not natively supported and have been skipped.\n" + + "You need to install a suitable plugin for your async framework, for example:\n" ) + msg += " - anyio\n" msg += " - pytest-asyncio\n" msg += " - pytest-tornasync\n" @@ -207,16 +208,17 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: def pytest_collect_file(file_path: Path, parent: nodes.Collector) -> Optional["Module"]: if file_path.suffix == ".py": - if not parent.session.isinitpath(file_path): - if not path_matches_patterns( - file_path, parent.config.getini("python_files") + ["__init__.py"] - ): - return None + if not parent.session.isinitpath( + file_path + ) and not path_matches_patterns( + file_path, parent.config.getini("python_files") + ["__init__.py"] + ): + return None ihook = parent.session.gethookproxy(file_path) - module: Module = ihook.pytest_pycollect_makemodule( + return ihook.pytest_pycollect_makemodule( module_path=file_path, parent=parent ) - return module + return None @@ -227,10 +229,8 @@ def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool: def pytest_pycollect_makemodule(module_path: Path, parent) -> "Module": if module_path.name == "__init__.py": - pkg: Package = Package.from_parent(parent, path=module_path) - return pkg - mod: Module = Module.from_parent(parent, path=module_path) - return mod + return Package.from_parent(parent, path=module_path) + return Module.from_parent(parent, path=module_path) @hookimpl(trylast=True) @@ -241,15 +241,16 @@ def pytest_pycollect_makeitem( # Nothing was collected elsewhere, let's do it here. if safe_isclass(obj): if collector.istestclass(obj, name): - klass: Class = Class.from_parent(collector, name=name, obj=obj) - return klass + return Class.from_parent(collector, name=name, obj=obj) elif collector.istestfunction(obj, name): # mock seems to store unbound methods (issue473), normalize it. obj = getattr(obj, "__func__", obj) # We need to try and unwrap the function if it's a functools.partial # or a functools.wrapped. # We mustn't if it's been wrapped with mock.patch (python 2 only). - if not (inspect.isfunction(obj) or inspect.isfunction(get_real_func(obj))): + if not inspect.isfunction(obj) and not inspect.isfunction( + get_real_func(obj) + ): filename, lineno = getfslineno(obj) warnings.warn_explicit( message=PytestCollectionWarning( @@ -260,16 +261,15 @@ def pytest_pycollect_makeitem( lineno=lineno + 1, ) elif getattr(obj, "__test__", True): - if is_generator(obj): - res: Function = Function.from_parent(collector, name=name) - reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format( - name=name - ) - res.add_marker(MARK_GEN.xfail(run=False, reason=reason)) - res.warn(PytestCollectionWarning(reason)) - return res - else: + if not is_generator(obj): return list(collector._genfunctions(name, obj)) + res: Function = Function.from_parent(collector, name=name) + reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format( + name=name + ) + res.add_marker(MARK_GEN.xfail(run=False, reason=reason)) + res.warn(PytestCollectionWarning(reason)) + return res return None @@ -435,9 +435,7 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: # Avoid random getattrs and peek in the __dict__ instead. dicts = [getattr(self.obj, "__dict__", {})] if isinstance(self.obj, type): - for basecls in self.obj.__mro__: - dicts.append(basecls.__dict__) - + dicts.extend(basecls.__dict__ for basecls in self.obj.__mro__) # In each class, nodes should be definition ordered. # __dict__ is definition ordered. seen: Set[str] = set() @@ -716,9 +714,7 @@ def _recurse(self, direntry: "os.DirEntry[str]") -> bool: if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): return False norecursepatterns = self.config.getini("norecursedirs") - if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns): - return False - return True + return not any(fnmatch_ex(pat, fspath) for pat in norecursepatterns) def _collectfile( self, fspath: Path, handle_dupes: bool = True @@ -729,9 +725,10 @@ def _collectfile( fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink() ) ihook = self.session.gethookproxy(fspath) - if not self.session.isinitpath(fspath): - if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): - return () + if not self.session.isinitpath(fspath) and ihook.pytest_ignore_collect( + collection_path=fspath, config=self.config + ): + return () if handle_dupes: keepduplicates = self.config.getoption("keepduplicates") @@ -756,9 +753,12 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: path = Path(direntry.path) # We will visit our own __init__.py file, in which case we skip it. - if direntry.is_file(): - if direntry.name == "__init__.py" and path.parent == this_path: - continue + if ( + direntry.is_file() + and direntry.name == "__init__.py" + and path.parent == this_path + ): + continue parts_ = parts(direntry.path) if any( @@ -942,15 +942,13 @@ def __getattr__(name: str) -> object: def hasinit(obj: object) -> bool: - init: object = getattr(obj, "__init__", None) - if init: + if init := getattr(obj, "__init__", None): return init != object.__init__ return False def hasnew(obj: object) -> bool: - new: object = getattr(obj, "__new__", None) - if new: + if new := getattr(obj, "__new__", None): return new != object.__new__ return False @@ -1030,9 +1028,7 @@ def _idval(self, val: object, argname: str, idx: int) -> str: if idval is not None: return idval idval = self._idval_from_value(val) - if idval is not None: - return idval - return self._idval_from_argname(argname, idx) + return idval if idval is not None else self._idval_from_argname(argname, idx) def _idval_from_function( self, val: object, argname: str, idx: int @@ -1048,18 +1044,16 @@ def _idval_from_function( msg = "error raised while trying to determine id of parameter '{}' at position {}" msg = prefix + msg.format(argname, idx) raise ValueError(msg) from e - if id is None: - return None - return self._idval_from_value(id) + return None if id is None else self._idval_from_value(id) def _idval_from_hook(self, val: object, argname: str) -> Optional[str]: """Try to make an ID for a parameter in a ParameterSet by calling the :hook:`pytest_make_parametrize_id` hook.""" if self.config: - id: Optional[str] = self.config.hook.pytest_make_parametrize_id( + return self.config.hook.pytest_make_parametrize_id( config=self.config, val=val, argname=argname ) - return id + return None def _idval_from_value(self, val: object) -> Optional[str]: @@ -1077,9 +1071,7 @@ def _idval_from_value(self, val: object) -> Optional[str]: elif isinstance(val, enum.Enum): return str(val) elif isinstance(getattr(val, "__name__", None), str): - # Name of a class, function, module, etc. - name: str = getattr(val, "__name__") - return name + return getattr(val, "__name__") return None def _idval_from_value_required(self, val: object, idx: int) -> str: @@ -1105,7 +1097,7 @@ def _idval_from_value_required(self, val: object, idx: int) -> str: def _idval_from_argname(argname: str, idx: int) -> str: """Make an ID for a parameter in a ParameterSet from the argument name and the index of the ParameterSet.""" - return str(argname) + str(idx) + return argname + str(idx) @final @@ -1417,7 +1409,7 @@ def _validate_ids( num_ids = len(parametersets) # num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849 - if num_ids != len(parametersets) and num_ids != 0: + if num_ids not in [len(parametersets), 0]: msg = "In {}: {} parameter sets specified, with different number of ids: {}" fail(msg.format(func_name, len(parametersets), num_ids), pytrace=False) @@ -1448,11 +1440,10 @@ def _resolve_arg_value_types( for arg in indirect: if arg not in argnames: fail( - "In {}: indirect fixture '{}' doesn't exist".format( - self.function.__name__, arg - ), + f"In {self.function.__name__}: indirect fixture '{arg}' doesn't exist", pytrace=False, ) + valtypes[arg] = "params" else: fail( @@ -1480,11 +1471,10 @@ def _validate_if_using_arg_names( if arg not in self.fixturenames: if arg in default_arg_names: fail( - "In {}: function already takes an argument '{}' with a default value".format( - func_name, arg - ), + f"In {func_name}: function already takes an argument '{arg}' with a default value", pytrace=False, ) + else: if isinstance(indirect, Sequence): name = "fixture" if arg in indirect else "argument" @@ -1648,20 +1638,18 @@ def _showfixtures_main(config: Config, session: Session) -> None: available.sort() currentmodule = None for baseid, module, prettypath, argname, fixturedef in available: - if currentmodule != module: - if not module.startswith("_pytest."): - tw.line() - tw.sep("-", f"fixtures defined from {module}") - currentmodule = module + if currentmodule != module and not module.startswith("_pytest."): + tw.line() + tw.sep("-", f"fixtures defined from {module}") + currentmodule = module if verbose <= 0 and argname.startswith("_"): continue tw.write(f"{argname}", green=True) if fixturedef.scope != "function": - tw.write(" [%s scope]" % fixturedef.scope, cyan=True) + tw.write(f" [{fixturedef.scope} scope]", cyan=True) tw.write(f" -- {prettypath}", yellow=True) tw.write("\n") - doc = inspect.getdoc(fixturedef.func) - if doc: + if doc := inspect.getdoc(fixturedef.func): write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc) else: tw.line(" no docstring available", red=True) @@ -1792,25 +1780,25 @@ def setup(self) -> None: self._request._fillfixtures() def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: - if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False): - code = _pytest._code.Code.from_function(get_real_func(self.obj)) - path, firstlineno = code.path, code.firstlineno - traceback = excinfo.traceback - ntraceback = traceback.cut(path=path, firstlineno=firstlineno) - if ntraceback == traceback: - ntraceback = ntraceback.cut(path=path) - if ntraceback == traceback: - ntraceback = ntraceback.filter(filter_traceback) - if not ntraceback: - ntraceback = traceback - - excinfo.traceback = ntraceback.filter() + if not hasattr(self, "_obj") or self.config.getoption("fulltrace", False): + return + code = _pytest._code.Code.from_function(get_real_func(self.obj)) + path, firstlineno = code.path, code.firstlineno + traceback = excinfo.traceback + ntraceback = traceback.cut(path=path, firstlineno=firstlineno) + if ntraceback == traceback: + ntraceback = ntraceback.cut(path=path) + if ntraceback == traceback: + ntraceback = ntraceback.filter(filter_traceback) or traceback + excinfo.traceback = ntraceback.filter() # issue364: mark all but first and last frames to # only show a single-line message for each frame. - if self.config.getoption("tbstyle", "auto") == "auto": - if len(excinfo.traceback) > 2: - for entry in excinfo.traceback[1:-1]: - entry.set_repr_style("short") + if ( + self.config.getoption("tbstyle", "auto") == "auto" + and len(excinfo.traceback) > 2 + ): + for entry in excinfo.traceback[1:-1]: + entry.set_repr_style("short") # TODO: Type ignored -- breaks Liskov Substitution. def repr_failure( # type: ignore[override] diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 515d437f0d8..41f469324f2 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -55,7 +55,7 @@ def _compare_approx( max_sizes[0] = max(max_sizes[0], len(index)) max_sizes[1] = max(max_sizes[1], len(obtained)) max_sizes[2] = max(max_sizes[2], len(expected)) - explanation = [ + return [ f"comparison failed. Mismatched elements: {len(different_ids)} / {number_of_elements}:", f"Max absolute difference: {max_abs_diff}", f"Max relative difference: {max_rel_diff}", @@ -63,7 +63,6 @@ def _compare_approx( f"{indexes:<{max_sizes[0]}} | {obtained:<{max_sizes[1]}} | {expected:<{max_sizes[2]}}" for indexes, obtained, expected in message_list ] - return explanation # builtin pytest.approx helper @@ -135,11 +134,10 @@ def _check_type(self) -> None: def _recursive_sequence_map(f, x): """Recursively map a function over a sequence of arbitrary depth""" - if isinstance(x, (list, tuple)): - seq_type = type(x) - return seq_type(_recursive_sequence_map(f, xi) for xi in x) - else: + if not isinstance(x, (list, tuple)): return f(x) + seq_type = type(x) + return seq_type(_recursive_sequence_map(f, xi) for xi in x) class ApproxNumpy(ApproxBase): @@ -455,9 +453,7 @@ def __eq__(self, actual) -> bool: if math.isinf(abs(self.expected)): # type: ignore[arg-type] return False - # Return true if the two numbers are within the tolerance. - result: bool = abs(self.expected - actual) <= self.tolerance - return result + return abs(self.expected - actual) <= self.tolerance # Ignore type because of https://github.com/python/mypy/issues/4266. __hash__ = None # type: ignore @@ -486,9 +482,8 @@ def set_default(x, default): # If the user specified an absolute tolerance but not a relative one, # just return the absolute tolerance. - if self.rel is None: - if self.abs is not None: - return absolute_tolerance + if self.rel is None and self.abs is not None: + return absolute_tolerance # Figure out what the relative tolerance should be. ``self.rel`` is # either None or a value specified by the user. This is done after @@ -932,8 +927,11 @@ def raises( # noqa: F811 if not args: match: Optional[Union[str, Pattern[str]]] = kwargs.pop("match", None) if kwargs: - msg = "Unexpected keyword arguments passed to pytest.raises: " - msg += ", ".join(sorted(kwargs)) + msg = ( + "Unexpected keyword arguments passed to pytest.raises: " + + ", ".join(sorted(kwargs)) + ) + msg += "\nUse context-manager form instead?" raise TypeError(msg) return RaisesContext(expected_exception, message, match) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index d76ea020f19..fa051fd4619 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -291,23 +291,28 @@ def found_str(): return pformat([record.message for record in self], indent=2) # only check if we're not currently handling an exception - if exc_type is None and exc_val is None and exc_tb is None: - if self.expected_warning is not None: - if not any(issubclass(r.category, self.expected_warning) for r in self): - __tracebackhide__ = True + if ( + exc_type is None + and exc_val is None + and exc_tb is None + and self.expected_warning is not None + ): + if not any(issubclass(r.category, self.expected_warning) for r in self): + __tracebackhide__ = True + fail( + f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n" + f"The list of emitted warnings is: {found_str()}." + ) + elif self.match_expr is not None: + for r in self: + if issubclass( + r.category, self.expected_warning + ) and re.compile(self.match_expr).search(str(r.message)): + break + else: fail( - f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n" - f"The list of emitted warnings is: {found_str()}." - ) - elif self.match_expr is not None: - for r in self: - if issubclass(r.category, self.expected_warning): - if re.compile(self.match_expr).search(str(r.message)): - break - else: - fail( - f"""\ + f"""\ DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted. Regex: {self.match_expr} Emitted warnings: {found_str()}""" - ) + ) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index c35f7087e41..164f3b0c00d 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -48,9 +48,10 @@ def getworkerinfoline(node): except AttributeError: d = node.workerinfo ver = "%s.%s.%s" % d["version_info"][:3] - node._workerinfocache = s = "[{}] {} -- Python {} {}".format( - d["id"], d["sysplatform"], ver, d["executable"] - ) + node._workerinfocache = ( + s + ) = f'[{d["id"]}] {d["sysplatform"]} -- Python {ver} {d["executable"]}' + return s @@ -77,8 +78,7 @@ def __getattr__(self, key: str) -> Any: def toterminal(self, out: TerminalWriter) -> None: if hasattr(self, "node"): - worker_info = getworkerinfoline(self.node) - if worker_info: + if worker_info := getworkerinfoline(self.node): out.line(worker_info) longrepr = self.longrepr @@ -233,10 +233,14 @@ def _report_unserialization_failure( url = "https://github.com/pytest-dev/pytest/issues" stream = StringIO() pprint("-" * 100, stream=stream) - pprint("INTERNALERROR: Unknown entry type returned: %s" % type_name, stream=stream) - pprint("report_name: %s" % report_class, stream=stream) + pprint( + f"INTERNALERROR: Unknown entry type returned: {type_name}", + stream=stream, + ) + + pprint(f"report_name: {report_class}", stream=stream) pprint(reportdict, stream=stream) - pprint("Please report this bug at %s" % url, stream=stream) + pprint(f"Please report this bug at {url}", stream=stream) pprint("-" * 100, stream=stream) raise RuntimeError(stream.getvalue()) @@ -320,7 +324,6 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": duration = call.duration keywords = {x: 1 for x in item.keywords} excinfo = call.excinfo - sections = [] if not call.excinfo: outcome: Literal["passed", "failed", "skipped"] = "passed" longrepr: Union[ @@ -330,29 +333,33 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": str, TerminalRepr, ] = None - else: - if not isinstance(excinfo, ExceptionInfo): - outcome = "failed" - longrepr = excinfo - elif isinstance(excinfo.value, skip.Exception): - outcome = "skipped" - r = excinfo._getreprcrash() - if excinfo.value._use_item_location: - path, line = item.reportinfo()[:2] - assert line is not None - longrepr = os.fspath(path), line + 1, r.message - else: - longrepr = (str(r.path), r.lineno, r.message) + elif not isinstance(excinfo, ExceptionInfo): + outcome = "failed" + longrepr = excinfo + elif isinstance(excinfo.value, skip.Exception): + outcome = "skipped" + r = excinfo._getreprcrash() + if excinfo.value._use_item_location: + path, line = item.reportinfo()[:2] + assert line is not None + longrepr = os.fspath(path), line + 1, r.message else: - outcome = "failed" - if call.when == "call": - longrepr = item.repr_failure(excinfo) - else: # exception in setup or teardown - longrepr = item._repr_failure_py( - excinfo, style=item.config.getoption("tbstyle", "auto") - ) - for rwhen, key, content in item._report_sections: - sections.append((f"Captured {key} {rwhen}", content)) + longrepr = (str(r.path), r.lineno, r.message) + else: + outcome = "failed" + longrepr = ( + item.repr_failure(excinfo) + if call.when == "call" + else item._repr_failure_py( + excinfo, style=item.config.getoption("tbstyle", "auto") + ) + ) + + sections = [ + (f"Captured {key} {rwhen}", content) + for rwhen, key, content in item._report_sections + ] + return cls( item.nodeid, item.location, @@ -443,9 +450,7 @@ def pytest_report_from_serializable( return TestReport._from_json(data) elif data["$report_type"] == "CollectReport": return CollectReport._from_json(data) - assert False, "Unknown report_type unserialize data: {}".format( - data["$report_type"] - ) + assert False, f'Unknown report_type unserialize data: {data["$report_type"]}' return None @@ -474,12 +479,9 @@ def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]: return result def serialize_repr_crash( - reprcrash: Optional[ReprFileLocation], - ) -> Optional[Dict[str, Any]]: - if reprcrash is not None: - return attr.asdict(reprcrash) - else: - return None + reprcrash: Optional[ReprFileLocation], + ) -> Optional[Dict[str, Any]]: + return attr.asdict(reprcrash) if reprcrash is not None else None def serialize_exception_longrepr(rep: BaseReport) -> Dict[str, Any]: assert rep.longrepr is not None diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 584c3229d5f..2252beeb0cc 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -75,16 +75,14 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None: tr = terminalreporter dlist = [] for replist in tr.stats.values(): - for rep in replist: - if hasattr(rep, "duration"): - dlist.append(rep) + dlist.extend(rep for rep in replist if hasattr(rep, "duration")) if not dlist: return dlist.sort(key=lambda x: x.duration, reverse=True) # type: ignore[no-any-return] if not durations: tr.write_sep("=", "slowest durations") else: - tr.write_sep("=", "slowest %s durations" % durations) + tr.write_sep("=", f"slowest {durations} durations") dlist = dlist[:durations] for i, rep in enumerate(dlist): @@ -144,9 +142,8 @@ def show_test_item(item: Item) -> None: tw.line() tw.write(" " * 8) tw.write(item.nodeid) - used_fixtures = sorted(getattr(item, "fixturenames", [])) - if used_fixtures: - tw.write(" (fixtures used: {})".format(", ".join(used_fixtures))) + if used_fixtures := sorted(getattr(item, "fixturenames", [])): + tw.write(f' (fixtures used: {", ".join(used_fixtures)})') tw.flush() @@ -236,10 +233,7 @@ def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> if hasattr(report, "wasxfail"): # Exception was expected. return False - if isinstance(call.excinfo.value, (Skipped, bdb.BdbQuit)): - # Special control flow exception. - return False - return True + return not isinstance(call.excinfo.value, (Skipped, bdb.BdbQuit)) def call_runtest_hook( @@ -391,7 +385,7 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport: assert isinstance(errorinfo, str) errorinfo = CollectErrorRepr(errorinfo) longrepr = errorinfo - result = call.result if not call.excinfo else None + result = None if call.excinfo else call.result rep = CollectReport(collector.nodeid, outcome, longrepr, result) rep.call = call # type: ignore # see collect_one_node return rep @@ -513,9 +507,10 @@ def teardown_exact(self, nextitem: Optional[Item]) -> None: """ needed_collectors = nextitem and nextitem.listchain() or [] exc = None - while self.stack: - if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: - break + while ( + self.stack + and list(self.stack.keys()) != needed_collectors[: len(self.stack)] + ): node, (finalizers, _) = self.stack.popitem() while finalizers: fin = finalizers.pop() diff --git a/src/_pytest/scope.py b/src/_pytest/scope.py index 7a746fb9fa9..721241bbbda 100644 --- a/src/_pytest/scope.py +++ b/src/_pytest/scope.py @@ -75,11 +75,10 @@ def from_user( scope = Scope(scope_name) except ValueError: fail( - "{} {}got an unexpected scope value '{}'".format( - descr, f"from {where} " if where else "", scope_name - ), + f"""{descr} {f"from {where} " if where else ""}got an unexpected scope value '{scope_name}'""", pytrace=False, ) + return scope diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index 583590d6b70..26cc9e418f3 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -77,9 +77,10 @@ def _show_fixture_action(fixturedef: FixtureDef[object], msg: str) -> None: ) if msg == "SETUP": - deps = sorted(arg for arg in fixturedef.argnames if arg != "request") - if deps: - tw.write(" (fixtures used: {})".format(", ".join(deps))) + if deps := sorted( + arg for arg in fixturedef.argnames if arg != "request" + ): + tw.write(f' (fixtures used: {", ".join(deps)})') if hasattr(fixturedef, "cached_param"): tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]") # type: ignore[attr-defined] diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index b20442350d6..650e1c99691 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -108,7 +108,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, dictionary ) ) - globals_.update(dictionary) + globals_ |= dictionary if hasattr(item, "obj"): globals_.update(item.obj.__globals__) # type: ignore[attr-defined] try: @@ -118,20 +118,21 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, except SyntaxError as exc: msglines = [ "Error evaluating %r condition" % mark.name, - " " + condition, + f" {condition}", " " + " " * (exc.offset or 0) + "^", "SyntaxError: invalid syntax", ] + fail("\n".join(msglines), pytrace=False) except Exception as exc: msglines = [ "Error evaluating %r condition" % mark.name, - " " + condition, + f" {condition}", *traceback.format_exception_only(type(exc), exc), ] + fail("\n".join(msglines), pytrace=False) - # Boolean condition. else: try: result = bool(condition) @@ -145,7 +146,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, reason = mark.kwargs.get("reason", None) if reason is None: if isinstance(condition, str): - reason = "condition: " + condition + reason = f"condition: {condition}" else: # XXX better be checked at collection time msg = ( @@ -187,7 +188,7 @@ def evaluate_skip_marks(item: Item) -> Optional[Skip]: try: return Skip(*mark.args, **mark.kwargs) except TypeError as e: - raise TypeError(str(e) + " - maybe you meant pytest.mark.skipif?") from None + raise TypeError(f"{str(e)} - maybe you meant pytest.mark.skipif?") from None return None @@ -233,13 +234,12 @@ def evaluate_xfail_marks(item: Item) -> Optional[Xfail]: @hookimpl(tryfirst=True) def pytest_runtest_setup(item: Item) -> None: - skipped = evaluate_skip_marks(item) - if skipped: + if skipped := evaluate_skip_marks(item): raise skip.Exception(skipped.reason, _use_item_location=True) item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) if xfailed and not item.config.option.runxfail and not xfailed.run: - xfail("[NOTRUN] " + xfailed.reason) + xfail(f"[NOTRUN] {xfailed.reason}") @hookimpl(hookwrapper=True) @@ -249,7 +249,7 @@ def pytest_runtest_call(item: Item) -> Generator[None, None, None]: item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) if xfailed and not item.config.option.runxfail and not xfailed.run: - xfail("[NOTRUN] " + xfailed.reason) + xfail(f"[NOTRUN] {xfailed.reason}") yield @@ -268,7 +268,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]): pass # don't interfere elif call.excinfo and isinstance(call.excinfo.value, xfail.Exception): assert call.excinfo.value.msg is not None - rep.wasxfail = "reason: " + call.excinfo.value.msg + rep.wasxfail = f"reason: {call.excinfo.value.msg}" rep.outcome = "skipped" elif not rep.skipped and xfailed: if call.excinfo: @@ -281,7 +281,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]): elif call.when == "call": if xfailed.strict: rep.outcome = "failed" - rep.longrepr = "[XPASS(strict)] " + xfailed.reason + rep.longrepr = f"[XPASS(strict)] {xfailed.reason}" else: rep.outcome = "passed" rep.wasxfail = xfailed.reason diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 84f1a6ce8fe..02ac1554a0c 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -72,12 +72,14 @@ def pytest_collection_modifyitems( self.report_status = "no previously failed tests, not skipping." return - # check all item nodes until we find a match on last failed - failed_index = None - for index, item in enumerate(items): - if item.nodeid == self.lastfailed: - failed_index = index - break + failed_index = next( + ( + index + for index, item in enumerate(items) + if item.nodeid == self.lastfailed + ), + None, + ) # If the previously failed test was not found among the test items, # do not skip any tests. @@ -106,12 +108,8 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: "Test failed, continuing from this test next run." ) - else: - # If the test was actually run and did pass. - if report.when == "call": - # Remove test from the failed ones, if exists. - if report.nodeid == self.lastfailed: - self.lastfailed = None + elif report.when == "call" and report.nodeid == self.lastfailed: + self.lastfailed = None def pytest_report_collectionfinish(self) -> Optional[str]: if self.config.getoption("verbose") >= 0 and self.report_status: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 9739a467a66..c4b7f9237ee 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -235,7 +235,7 @@ def pytest_configure(config: Config) -> None: def mywriter(tags, args): msg = " ".join(map(str, args)) - reporter.write_line("[traceconfig] " + msg) + reporter.write_line(f"[traceconfig] {msg}") config.trace.root.setprocessor("pytest:config", mywriter) @@ -258,7 +258,7 @@ def getreportopt(config: Config) -> str: reportopts += char if not config.option.disable_warnings and "w" not in reportopts: - reportopts = "w" + reportopts + reportopts = f"w{reportopts}" elif config.option.disable_warnings and "w" in reportopts: reportopts = reportopts.replace("w", "") @@ -356,8 +356,7 @@ def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]" @property def verbosity(self) -> int: - verbosity: int = self.config.option.verbose - return verbosity + return self.config.option.verbose @property def showheader(self) -> bool: @@ -373,9 +372,7 @@ def no_summary(self) -> bool: @property def showfspath(self) -> bool: - if self._showfspath is None: - return self.verbosity >= 0 - return self._showfspath + return self.verbosity >= 0 if self._showfspath is None else self._showfspath @showfspath.setter def showfspath(self, value: Optional[bool]) -> None: @@ -397,7 +394,7 @@ def write_fspath_result(self, nodeid: str, res, **markup: bool) -> None: self.currentfspath = fspath relfspath = bestrelpath(self.startpath, fspath) self._tw.line() - self._tw.write(relfspath + " ") + self._tw.write(f"{relfspath} ") self._tw.write(res, flush=True, **markup) def write_ensure_prefix(self, prefix: str, extra: str = "", **kwargs) -> None: @@ -441,7 +438,7 @@ def rewrite(self, line: str, **markup: bool) -> None: fill = " " * fill_count else: fill = "" - line = str(line) + line = line self._tw.write("\r" + line + fill, **markup) def write_sep( @@ -468,7 +465,7 @@ def _add_stats(self, category: str, items: Sequence[Any]) -> None: def pytest_internalerror(self, excrepr: ExceptionRepr) -> bool: for line in str(excrepr).split("\n"): - self.write_line("INTERNALERROR> " + line) + self.write_line(f"INTERNALERROR> {line}") return True def pytest_warning_recorded( @@ -530,12 +527,10 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: was_xfail = hasattr(report, "wasxfail") if rep.passed and not was_xfail: markup = {"green": True} - elif rep.passed and was_xfail: + elif rep.passed or not rep.failed and rep.skipped: markup = {"yellow": True} elif rep.failed: markup = {"red": True} - elif rep.skipped: - markup = {"yellow": True} else: markup = {} if self.verbosity <= 0: @@ -565,15 +560,13 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: self._write_progress_information_filling_space() else: self.ensure_newline() - self._tw.write("[%s]" % rep.node.gateway.id) + self._tw.write(f"[{rep.node.gateway.id}]") if self._show_progress_info: - self._tw.write( - self._get_progress_information_message() + " ", cyan=True - ) + self._tw.write(f"{self._get_progress_information_message()} ", cyan=True) else: self._tw.write(" ") self._tw.write(word, **markup) - self._tw.write(" " + line) + self._tw.write(f" {line}") self.currentfspath = -2 self.flush() @@ -606,19 +599,21 @@ def pytest_runtest_logfinish(self, nodeid: str) -> None: def _get_progress_information_message(self) -> str: assert self._session collected = self._session.testscollected - if self._show_progress_info == "count": - if collected: - progress = self._progress_nodeids_reported - counter_format = f"{{:{len(str(collected))}d}}" - format_string = f" [{counter_format}/{{}}]" - return format_string.format(len(progress), collected) - return f" [ {collected} / {collected} ]" - else: - if collected: - return " [{:3d}%]".format( + if self._show_progress_info != "count": + return ( + " [{:3d}%]".format( len(self._progress_nodeids_reported) * 100 // collected ) - return " [100%]" + if collected + else " [100%]" + ) + + if collected: + progress = self._progress_nodeids_reported + counter_format = f"{{:{len(str(collected))}d}}" + format_string = f" [{counter_format}/{{}}]" + return format_string.format(len(progress), collected) + return f" [ {collected} / {collected} ]" def _write_progress_information_filling_space(self) -> None: color, _ = self._get_main_color() @@ -669,9 +664,10 @@ def report_collect(self, final: bool = False) -> None: deselected = len(self.stats.get("deselected", [])) selected = self._numcollected - deselected line = "collected " if final else "collecting " - line += ( - str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s") + line += f"{str(self._numcollected)} item" + ( + "" if self._numcollected == 1 else "s" ) + if errors: line += " / %d error%s" % (errors, "s" if errors != 1 else "") if deselected: @@ -697,19 +693,16 @@ def pytest_sessionstart(self, session: "Session") -> None: verinfo = platform.python_version() if not self.no_header: msg = f"platform {sys.platform} -- Python {verinfo}" - pypy_version_info = getattr(sys, "pypy_version_info", None) - if pypy_version_info: + if pypy_version_info := getattr(sys, "pypy_version_info", None): verinfo = ".".join(map(str, pypy_version_info[:3])) msg += f"[pypy-{verinfo}-{pypy_version_info[3]}]" - msg += ", pytest-{}, pluggy-{}".format( - _pytest._version.version, pluggy.__version__ - ) + msg += f", pytest-{_pytest._version.version}, pluggy-{pluggy.__version__}" if ( self.verbosity > 0 or self.config.option.debug or getattr(self.config.option, "pastebin", None) ): - msg += " -- " + str(sys.executable) + msg += f" -- {str(sys.executable)}" self.write_line(msg) lines = self.config.hook.pytest_report_header( config=self.config, start_path=self.startpath @@ -727,20 +720,19 @@ def _write_report_lines_from_hooks( self.write_line(line) def pytest_report_header(self, config: Config) -> List[str]: - line = "rootdir: %s" % config.rootpath + line = f"rootdir: {config.rootpath}" if config.inipath: - line += ", configfile: " + bestrelpath(config.rootpath, config.inipath) + line += f", configfile: {bestrelpath(config.rootpath, config.inipath)}" if config.args_source == Config.ArgsSource.TESTPATHS: testpaths: List[str] = config.getini("testpaths") - line += ", testpaths: {}".format(", ".join(testpaths)) + line += f', testpaths: {", ".join(testpaths)}' result = [line] - plugininfo = config.pluginmanager.list_plugin_distinfo() - if plugininfo: - result.append("plugins: %s" % ", ".join(_plugin_nameversions(plugininfo))) + if plugininfo := config.pluginmanager.list_plugin_distinfo(): + result.append(f'plugins: {", ".join(_plugin_nameversions(plugininfo))}') return result def pytest_collection_finish(self, session: "Session") -> None: @@ -759,8 +751,7 @@ def pytest_collection_finish(self, session: "Session") -> None: self._tw.line("") self._printcollecteditems(session.items) - failed = self.stats.get("failed") - if failed: + if failed := self.stats.get("failed"): self._tw.sep("!", "collection failures") for rep in failed: rep.toterminal(self._tw) @@ -779,9 +770,7 @@ def _printcollecteditems(self, items: Sequence[Item]) -> None: indent = "" for item in items: needed_collectors = item.listchain()[1:] # strip root node - while stack: - if stack == needed_collectors[: len(stack)]: - break + while stack and stack != needed_collectors[: len(stack)]: stack.pop() for col in needed_collectors[len(stack) :]: stack.append(col) @@ -792,7 +781,7 @@ def _printcollecteditems(self, items: Sequence[Item]) -> None: doc = inspect.getdoc(obj) if obj else None if doc: for line in doc.splitlines(): - self._tw.line("{}{}".format(indent + " ", line)) + self._tw.line(f"{indent} {line}") @hookimpl(hookwrapper=True) def pytest_sessionfinish( @@ -873,16 +862,13 @@ def mkrel(nodeid: str) -> str: if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace( "\\", nodes.SEP ): - res += " <- " + bestrelpath(self.startpath, Path(fspath)) + res += f" <- {bestrelpath(self.startpath, Path(fspath))}" else: res = "[location]" - return res + " " + return f"{res} " def _getfailureheadline(self, rep): - head_line = rep.head_line - if head_line: - return head_line - return "test session" # XXX? + return head_line if (head_line := rep.head_line) else "test session" def _getcrashline(self, rep): try: @@ -900,72 +886,74 @@ def getreports(self, name: str): return [x for x in self.stats.get(name, ()) if not hasattr(x, "_pdbshown")] def summary_warnings(self) -> None: - if self.hasopt("w"): - all_warnings: Optional[List[WarningReport]] = self.stats.get("warnings") - if not all_warnings: - return - - final = self._already_displayed_warnings is not None - if final: - warning_reports = all_warnings[self._already_displayed_warnings :] - else: - warning_reports = all_warnings - self._already_displayed_warnings = len(warning_reports) - if not warning_reports: - return + if not self.hasopt("w"): + return + all_warnings: Optional[List[WarningReport]] = self.stats.get("warnings") + if not all_warnings: + return - reports_grouped_by_message: Dict[str, List[WarningReport]] = {} - for wr in warning_reports: - reports_grouped_by_message.setdefault(wr.message, []).append(wr) + final = self._already_displayed_warnings is not None + if final: + warning_reports = all_warnings[self._already_displayed_warnings :] + else: + warning_reports = all_warnings + self._already_displayed_warnings = len(warning_reports) + if not warning_reports: + return - def collapsed_location_report(reports: List[WarningReport]) -> str: - locations = [] - for w in reports: - location = w.get_location(self.config) - if location: - locations.append(location) + reports_grouped_by_message: Dict[str, List[WarningReport]] = {} + for wr in warning_reports: + reports_grouped_by_message.setdefault(wr.message, []).append(wr) - if len(locations) < 10: - return "\n".join(map(str, locations)) + def collapsed_location_report(reports: List[WarningReport]) -> str: + locations = [] + for w in reports: + location = w.get_location(self.config) + if location: + locations.append(location) - counts_by_filename = Counter( - str(loc).split("::", 1)[0] for loc in locations - ) - return "\n".join( - "{}: {} warning{}".format(k, v, "s" if v > 1 else "") - for k, v in counts_by_filename.items() - ) + if len(locations) < 10: + return "\n".join(map(str, locations)) - title = "warnings summary (final)" if final else "warnings summary" - self.write_sep("=", title, yellow=True, bold=False) - for message, message_reports in reports_grouped_by_message.items(): - maybe_location = collapsed_location_report(message_reports) - if maybe_location: - self._tw.line(maybe_location) - lines = message.splitlines() - indented = "\n".join(" " + x for x in lines) - message = indented.rstrip() - else: - message = message.rstrip() - self._tw.line(message) - self._tw.line() - self._tw.line( - "-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html" + counts_by_filename = Counter( + str(loc).split("::", 1)[0] for loc in locations ) + return "\n".join( + "{}: {} warning{}".format(k, v, "s" if v > 1 else "") + for k, v in counts_by_filename.items() + ) + + title = "warnings summary (final)" if final else "warnings summary" + self.write_sep("=", title, yellow=True, bold=False) + for message, message_reports in reports_grouped_by_message.items(): + maybe_location = collapsed_location_report(message_reports) + if maybe_location: + self._tw.line(maybe_location) + lines = message.splitlines() + indented = "\n".join(f" {x}" for x in lines) + message = indented.rstrip() + else: + message = message.rstrip() + self._tw.line(message) + self._tw.line() + self._tw.line( + "-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html" + ) def summary_passes(self) -> None: - if self.config.option.tbstyle != "no": - if self.hasopt("P"): - reports: List[TestReport] = self.getreports("passed") - if not reports: - return - self.write_sep("=", "PASSES") - for rep in reports: - if rep.sections: - msg = self._getfailureheadline(rep) - self.write_sep("_", msg, green=True, bold=True) - self._outrep_summary(rep) - self._handle_teardown_sections(rep.nodeid) + if self.config.option.tbstyle == "no": + return + if self.hasopt("P"): + reports: List[TestReport] = self.getreports("passed") + if not reports: + return + self.write_sep("=", "PASSES") + for rep in reports: + if rep.sections: + msg = self._getfailureheadline(rep) + self.write_sep("_", msg, green=True, bold=True) + self._outrep_summary(rep) + self._handle_teardown_sections(rep.nodeid) def _get_teardown_reports(self, nodeid: str) -> List[TestReport]: reports = self.getreports("") @@ -993,36 +981,38 @@ def print_teardown_sections(self, rep: TestReport) -> None: self._tw.line(content) def summary_failures(self) -> None: - if self.config.option.tbstyle != "no": - reports: List[BaseReport] = self.getreports("failed") - if not reports: - return - self.write_sep("=", "FAILURES") - if self.config.option.tbstyle == "line": - for rep in reports: - line = self._getcrashline(rep) - self.write_line(line) - else: - for rep in reports: - msg = self._getfailureheadline(rep) - self.write_sep("_", msg, red=True, bold=True) - self._outrep_summary(rep) - self._handle_teardown_sections(rep.nodeid) - - def summary_errors(self) -> None: - if self.config.option.tbstyle != "no": - reports: List[BaseReport] = self.getreports("error") - if not reports: - return - self.write_sep("=", "ERRORS") - for rep in self.stats["error"]: + if self.config.option.tbstyle == "no": + return + reports: List[BaseReport] = self.getreports("failed") + if not reports: + return + self.write_sep("=", "FAILURES") + if self.config.option.tbstyle == "line": + for rep in reports: + line = self._getcrashline(rep) + self.write_line(line) + else: + for rep in reports: msg = self._getfailureheadline(rep) - if rep.when == "collect": - msg = "ERROR collecting " + msg - else: - msg = f"ERROR at {rep.when} of {msg}" self.write_sep("_", msg, red=True, bold=True) self._outrep_summary(rep) + self._handle_teardown_sections(rep.nodeid) + + def summary_errors(self) -> None: + if self.config.option.tbstyle == "no": + return + reports: List[BaseReport] = self.getreports("error") + if not reports: + return + self.write_sep("=", "ERRORS") + for rep in self.stats["error"]: + msg = self._getfailureheadline(rep) + if rep.when == "collect": + msg = f"ERROR collecting {msg}" + else: + msg = f"ERROR at {rep.when} of {msg}" + self.write_sep("_", msg, red=True, bold=True) + self._outrep_summary(rep) def _outrep_summary(self, rep: BaseReport) -> None: rep.toterminal(self._tw) @@ -1101,7 +1091,7 @@ def show_xfailed(lines: List[str]) -> None: line = f"{markup_word} {nodeid}" reason = rep.wasxfail if reason: - line += " - " + str(reason) + line += f" - {str(reason)}" lines.append(line) @@ -1166,21 +1156,23 @@ def _get_main_color(self) -> Tuple[str, List[str]]: def _determine_main_color(self, unknown_type_seen: bool) -> str: stats = self.stats if "failed" in stats or "error" in stats: - main_color = "red" + return "red" elif "warnings" in stats or "xpassed" in stats or unknown_type_seen: - main_color = "yellow" + return "yellow" elif "passed" in stats or not self._is_last_item: - main_color = "green" + return "green" else: - main_color = "yellow" - return main_color + return "yellow" def _set_main_color(self) -> None: unknown_types: List[str] = [] for found_type in self.stats.keys(): - if found_type: # setup/teardown reports have an empty key, ignore them - if found_type not in KNOWN_TYPES and found_type not in unknown_types: - unknown_types.append(found_type) + if ( + found_type + and found_type not in KNOWN_TYPES + and found_type not in unknown_types + ): + unknown_types.append(found_type) self._known_types = list(KNOWN_TYPES) + unknown_types self._main_color = self._determine_main_color(bool(unknown_types)) @@ -1221,8 +1213,7 @@ def _build_normal_summary_stats_line( parts = [] for key in known_types: - reports = self._get_reports_to_display(key) - if reports: + if reports := self._get_reports_to_display(key): count = len(reports) color = _color_for_type.get(key, _color_for_type_default) markup = {color: True, "bold": color == main_color} @@ -1271,7 +1262,7 @@ def _get_node_id_with_markup(tw: TerminalWriter, config: Config, rep: BaseReport path, *parts = nodeid.split("::") if parts: parts_markup = tw.markup("::".join(parts), bold=True) - return path + "::" + parts_markup + return f"{path}::{parts_markup}" else: return path @@ -1354,10 +1345,7 @@ def _folded_skips( else: key = (fspath, lineno, reason) d.setdefault(key, []).append(event) - values: List[Tuple[int, str, Optional[int], str]] = [] - for key, events in d.items(): - values.append((len(events), *key)) - return values + return [(len(events), *key) for key, events in d.items()] _color_for_type = { @@ -1379,7 +1367,7 @@ def pluralize(count: int, noun: str) -> Tuple[int, str]: # for `error`. noun = noun.replace("warnings", "warning") - return count, noun + "s" if count != 1 else noun + return count, f"{noun}s" if count != 1 else noun def _plugin_nameversions(plugininfo) -> List[str]: @@ -1388,8 +1376,7 @@ def _plugin_nameversions(plugininfo) -> List[str]: # Gets us name and version! name = "{dist.project_name}-{dist.version}".format(dist=dist) # Questionable convenience, but it keeps things short. - if name.startswith("pytest-"): - name = name[7:] + name = name.removeprefix("pytest-") # We decided to print python package names they can have more than one plugin. if name not in values: values.append(name) @@ -1400,9 +1387,8 @@ def format_session_duration(seconds: float) -> str: """Format the given seconds in a human readable manner to show in the final summary.""" if seconds < 60: return f"{seconds:.2f}s" - else: - dt = datetime.timedelta(seconds=int(seconds)) - return f"{seconds:.2f}s ({dt})" + dt = datetime.timedelta(seconds=int(seconds)) + return f"{seconds:.2f}s ({dt})" def _get_raw_skip_reason(report: TestReport) -> str: @@ -1414,7 +1400,6 @@ def _get_raw_skip_reason(report: TestReport) -> str: reason = cast(str, report.wasxfail) if reason.startswith("reason: "): reason = reason[len("reason: ") :] - return reason else: assert report.skipped assert isinstance(report.longrepr, tuple) @@ -1423,4 +1408,5 @@ def _get_raw_skip_reason(report: TestReport) -> str: reason = reason[len("Skipped: ") :] elif reason == "Skipped": reason = "" - return reason + + return reason diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index c2df986530c..bfd60229b45 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -52,9 +52,7 @@ def pytest_pycollect_makeitem( return None except Exception: return None - # Yes, so let's collect it. - item: UnitTestCase = UnitTestCase.from_parent(collector, name=name, obj=obj) - return item + return UnitTestCase.from_parent(collector, name=name, obj=obj) class UnitTestCase(Class): @@ -96,26 +94,24 @@ def collect(self) -> Iterable[Union[Item, Collector]]: def _inject_setup_teardown_fixtures(self, cls: type) -> None: """Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding teardown functions (#517).""" - class_fixture = _make_xunit_fixture( + if class_fixture := _make_xunit_fixture( cls, "setUpClass", "tearDownClass", "doClassCleanups", scope=Scope.Class, pass_self=False, - ) - if class_fixture: + ): cls.__pytest_class_setup = class_fixture # type: ignore[attr-defined] - method_fixture = _make_xunit_fixture( + if method_fixture := _make_xunit_fixture( cls, "setup_method", "teardown_method", None, scope=Scope.Function, pass_self=True, - ) - if method_fixture: + ): cls.__pytest_method_setup = method_fixture # type: ignore[attr-defined] @@ -274,7 +270,7 @@ def addExpectedFailure( reason: str = "", ) -> None: try: - xfail(str(reason)) + xfail(reason) except xfail.Exception: self._addexcinfo(sys.exc_info()) @@ -335,22 +331,20 @@ def _prunetraceback( self, excinfo: _pytest._code.ExceptionInfo[BaseException] ) -> None: super()._prunetraceback(excinfo) - traceback = excinfo.traceback.filter( + if traceback := excinfo.traceback.filter( lambda x: not x.frame.f_globals.get("__unittest") - ) - if traceback: + ): excinfo.traceback = traceback @hookimpl(tryfirst=True) def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: - if isinstance(item, TestCaseFunction): - if item._excinfo: - call.excinfo = item._excinfo.pop(0) - try: - del call.result - except AttributeError: - pass + if isinstance(item, TestCaseFunction) and item._excinfo: + call.excinfo = item._excinfo.pop(0) + try: + del call.result + except AttributeError: + pass # Convert unittest.SkipTest to pytest.skip. # This is actually only needed for nose, which reuses unittest.SkipTest for @@ -414,4 +408,4 @@ def check_testcase_implements_trial_reporter(done: List[int] = []) -> None: def _is_skipped(obj) -> bool: """Return True if the given object has been marked with @unittest.skip.""" - return bool(getattr(obj, "__unittest_skip__", False)) + return getattr(obj, "__unittest_skip__", False) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index c7139b538b2..3ebbb7f3757 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -12,8 +12,7 @@ def prepend_pythonpath(*dirs) -> str: - cur = os.getenv("PYTHONPATH") - if cur: + if cur := os.getenv("PYTHONPATH"): dirs += (cur,) return os.pathsep.join(str(p) for p in dirs) @@ -307,7 +306,7 @@ def pytest_collect_file(file_path, parent): return MyCollector.from_parent(path=file_path, parent=parent) """ ) - result = pytester.runpytest(c.name + "::" + "xyz") + result = pytester.runpytest(f"{c.name}::xyz") assert result.ret == 0 result.stdout.fnmatch_lines(["*1 pass*"]) @@ -338,7 +337,7 @@ def test_func(i): pass """ ) - res = pytester.runpytest(p.name + "::" + "test_func[1]") + res = pytester.runpytest(f"{p.name}::test_func[1]") assert res.ret == 0 res.stdout.fnmatch_lines(["*1 passed*"]) @@ -349,7 +348,7 @@ def test_func(): pass """ ) - res = pytester.runpytest(p.name + "::" + "test_notfound") + res = pytester.runpytest(f"{p.name}::test_notfound") assert res.ret res.stderr.fnmatch_lines(["*ERROR*not found*"]) @@ -358,7 +357,7 @@ def test_docstring_on_hookspec(self) -> None: for name, value in vars(hookspec).items(): if name.startswith("pytest_"): - assert value.__doc__, "no docstring for %s" % name + assert value.__doc__, f"no docstring for {name}" def test_initialization_error_issue49(self, pytester: Pytester) -> None: pytester.makeconftest( @@ -723,7 +722,7 @@ def test(): pass """ ) - result = pytester.runpytest(str(p) + "::test", "--doctest-modules") + result = pytester.runpytest(f"{str(p)}::test", "--doctest-modules") result.stdout.fnmatch_lines(["*1 passed*"]) def test_cmdline_python_package_symlink( @@ -889,7 +888,7 @@ def test_calls_showall(self, pytester: Pytester, mock_timing) -> None: for x in tested: for y in ("call",): # 'setup', 'call', 'teardown': for line in result.stdout.lines: - if ("test_%s" % x) in line and y in line: + if f"test_{x}" in line and y in line: break else: raise AssertionError(f"not found {x} {y}") @@ -902,7 +901,7 @@ def test_calls_showall_verbose(self, pytester: Pytester, mock_timing) -> None: for x in "123": for y in ("call",): # 'setup', 'call', 'teardown': for line in result.stdout.lines: - if ("test_%s" % x) in line and y in line: + if f"test_{x}" in line and y in line: break else: raise AssertionError(f"not found {x} {y}") diff --git a/testing/code/test_code.py b/testing/code/test_code.py index 33809528a06..23255e8d1cf 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -144,10 +144,7 @@ def f4(x, *y, **z) -> FrameType: class TestExceptionInfo: def test_bad_getsource(self) -> None: try: - if False: - pass - else: - assert False + assert False except AssertionError: exci = ExceptionInfo.from_current() assert exci.getrepr() @@ -160,10 +157,7 @@ def test_from_current_with_missing(self) -> None: class TestTracebackEntry: def test_getsource(self) -> None: try: - if False: - pass - else: - assert False + assert False except AssertionError: exci = ExceptionInfo.from_current() entry = exci.traceback[0] diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index e428b9c5ca9..c98f5b945c6 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -406,7 +406,7 @@ def test_codepath_Queue_example() -> None: def test_match_succeeds(): with pytest.raises(ZeroDivisionError) as excinfo: - 0 // 0 + 1 excinfo.match(r".*zero.*") @@ -577,7 +577,7 @@ def __repr__(self): assert "[ExceptionWithBrokenClass() raised in repr()]" in reprlocals.lines[1] def test_repr_local_truncated(self) -> None: - loc = {"l": [i for i in range(10)]} + loc = {"l": list(range(10))} p = FormattedExcinfo(showlocals=True) truncated_reprlocals = p.repr_locals(loc) assert truncated_reprlocals is not None @@ -1240,9 +1240,7 @@ def g(): assert tw_mock.lines[2] == " try:" assert tw_mock.lines[3] == " g()" assert tw_mock.lines[4] == " except Exception:" - assert tw_mock.lines[5] == "> raise AttributeError(){}".format( - raise_suffix - ) + assert tw_mock.lines[5] == f"> raise AttributeError(){raise_suffix}" assert tw_mock.lines[6] == "E AttributeError" assert tw_mock.lines[7] == "" line = tw_mock.get_write_msg(8) @@ -1290,7 +1288,7 @@ def g(): mod.f() # emulate the issue described in #1984 - attr = "__%s__" % reason + attr = f"__{reason}__" getattr(excinfo.value, attr).__traceback__ = None r = excinfo.getrepr() @@ -1369,10 +1367,7 @@ def f(): @pytest.mark.parametrize("style", ["short", "long"]) @pytest.mark.parametrize("encoding", [None, "utf8", "utf16"]) def test_repr_traceback_with_unicode(style, encoding): - if encoding is None: - msg: Union[str, bytes] = "☹" - else: - msg = "☹".encode(encoding) + msg = "☹" if encoding is None else "☹".encode(encoding) try: raise RuntimeError(msg) except RuntimeError: @@ -1463,9 +1458,12 @@ def test_no_recursion_index_on_recursion_error(): during a recursion error (#2486). """ + + class RecursionDepthError: def __getattr__(self, attr): - return getattr(self, "_" + attr) + return getattr(self, f"_{attr}") + with pytest.raises(RuntimeError) as excinfo: RecursionDepthError().trigger @@ -1478,10 +1476,10 @@ def _exceptiongroup_common( inner_chain: str, native: bool, ) -> None: - pre_raise = "exceptiongroup." if not native else "" + pre_raise = "" if native else "exceptiongroup." pre_catch = pre_raise if sys.version_info < (3, 11) else "" filestr = f""" - {"import exceptiongroup" if not native else ""} + {"" if native else "import exceptiongroup"} import pytest def f(): raise ValueError("From f()") @@ -1520,10 +1518,11 @@ def outer(outer_chain, inner_chain): def test(): outer("{outer_chain}", "{inner_chain}") """ + pytester.makepyfile(test_excgroup=filestr) result = pytester.runpytest() match_lines = [] - if inner_chain in ("another", "from"): + if inner_chain in {"another", "from"}: match_lines.append(r"SyntaxError: ") match_lines += [ @@ -1533,7 +1532,7 @@ def test(): r" \| BaseException: From g\(\)", r"=* short test summary info =*", ] - if outer_chain in ("another", "from"): + if outer_chain in {"another", "from"}: match_lines.append(r"FAILED test_excgroup.py::test - IndexError") else: match_lines.append( diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 52417f2f837..41600834f17 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -63,7 +63,7 @@ def f(): def test_source_strips() -> None: source = Source("") assert source == Source() - assert str(source) == "" + assert not str(source) assert source.strip() == source @@ -86,7 +86,7 @@ def g(x): ) def test_getrange(self) -> None: - x = self.source[0:2] + x = self.source[:2] assert len(x.lines) == 2 assert str(x) == "def f(x):\n pass" @@ -102,7 +102,7 @@ def test_len(self) -> None: assert len(self.source) == 4 def test_iter(self) -> None: - values = [x for x in self.source] + values = list(self.source) assert len(values) == 4 diff --git a/testing/conftest.py b/testing/conftest.py index 107aad86b25..2f61cdfeeaf 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -52,12 +52,10 @@ def pytest_collection_modifyitems(items): else: slow_items.append(item) item.add_marker(pytest.mark.slow) + elif marker := item.get_closest_marker("slow"): + slowest_items.append(item) else: - marker = item.get_closest_marker("slow") - if marker: - slowest_items.append(item) - else: - fast_items.append(item) + fast_items.append(item) items[:] = fast_items + neutral_items + slow_items + slowest_items diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 3ceed7f5a2c..ab36f79f4e9 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -139,11 +139,7 @@ def test_hookproxy_warnings_for_pathlib(tmp_path, hooktype, request): path = legacy_path(tmp_path) PATH_WARN_MATCH = r".*path: py\.path\.local\) argument is deprecated, please use \(collection_path: pathlib\.Path.*" - if hooktype == "ihook": - hooks = request.node.ihook - else: - hooks = request.config.hook - + hooks = request.node.ihook if hooktype == "ihook" else request.config.hook with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r: l1 = sys._getframe().f_lineno hooks.pytest_ignore_collect( diff --git a/testing/example_scripts/doctest/main_py/__main__.py b/testing/example_scripts/doctest/main_py/__main__.py index e471d06d643..7e0525f69fb 100644 --- a/testing/example_scripts/doctest/main_py/__main__.py +++ b/testing/example_scripts/doctest/main_py/__main__.py @@ -1,2 +1,2 @@ def test_this_is_ignored(): - assert True + pass diff --git a/testing/example_scripts/issue_519.py b/testing/example_scripts/issue_519.py index e44367fca04..630c3a8634b 100644 --- a/testing/example_scripts/issue_519.py +++ b/testing/example_scripts/issue_519.py @@ -36,13 +36,13 @@ def checked_order(): @pytest.fixture(scope="module") def fix1(request, arg1, checked_order): checked_order.append((request.node.name, "fix1", arg1)) - yield "fix1-" + arg1 + yield f"fix1-{arg1}" @pytest.fixture(scope="function") def fix2(request, fix1, arg2, checked_order): checked_order.append((request.node.name, "fix2", arg2)) - yield "fix2-" + arg2 + fix1 + yield f"fix2-{arg2}{fix1}" def test_one(fix2): diff --git a/testing/example_scripts/tmpdir/tmp_path_fixture.py b/testing/example_scripts/tmpdir/tmp_path_fixture.py index 8675eb2fa62..d97f9fb07bc 100644 --- a/testing/example_scripts/tmpdir/tmp_path_fixture.py +++ b/testing/example_scripts/tmpdir/tmp_path_fixture.py @@ -4,4 +4,4 @@ @pytest.mark.parametrize("a", [r"qwe/\abc"]) def test_fixture(tmp_path, a): assert tmp_path.is_dir() - assert list(tmp_path.iterdir()) == [] + assert not list(tmp_path.iterdir()) diff --git a/testing/freeze/tox_run.py b/testing/freeze/tox_run.py index 678a69c858a..1230fcce140 100644 --- a/testing/freeze/tox_run.py +++ b/testing/freeze/tox_run.py @@ -2,6 +2,7 @@ Called by tox.ini: uses the generated executable to run the tests in ./tests/ directory. """ + if __name__ == "__main__": import os import sys @@ -9,4 +10,4 @@ executable = os.path.join(os.getcwd(), "dist", "runtests_script", "runtests_script") if sys.platform.startswith("win"): executable += ".exe" - sys.exit(os.system("%s tests" % executable)) + sys.exit(os.system(f"{executable} tests")) diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index 24746bc2235..734eae3f3d2 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -146,9 +146,13 @@ def test_big_repr(): def test_repr_on_newstyle() -> None: + + + class Function: def __repr__(self): - return "<%s>" % (self.name) # type: ignore[attr-defined] + return f"<{self.name}>" + assert saferepr(Function()) diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index e9e73d05f98..de516d409a3 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -5,7 +5,7 @@ from _pytest.pytester import Pytester logger = logging.getLogger(__name__) -sublogger = logging.getLogger(__name__ + ".baz") +sublogger = logging.getLogger(f"{__name__}.baz") def test_fixture_help(pytester: Pytester) -> None: diff --git a/testing/logging/test_formatter.py b/testing/logging/test_formatter.py index 37971293726..6b2540af1b1 100644 --- a/testing/logging/test_formatter.py +++ b/testing/logging/test_formatter.py @@ -129,7 +129,7 @@ def test_multiline_message() -> None: ) # anything other than string or int will default to False - record.auto_indent = dict() + record.auto_indent = {} output = ai_off_style.format(record) assert output == ( "dummypath 10 INFO Test Message line1\nline2" diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 323ff7b2446..37636261c1f 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -243,7 +243,7 @@ def test_log_cli_default_level_multiple_tests( pytester: Pytester, request: FixtureRequest ) -> None: """Ensure we reset the first newline added by the live logger between tests""" - filename = request.node.name + ".py" + filename = f"{request.node.name}.py" pytester.makepyfile( """ import logging @@ -281,7 +281,7 @@ def test_log_cli_default_level_sections( ) -> None: """Check that with live logging enable we are printing the correct headers during start/setup/call/teardown/finish.""" - filename = request.node.name + ".py" + filename = f"{request.node.name}.py" pytester.makeconftest( """ import pytest @@ -357,7 +357,7 @@ def test_live_logs_unknown_sections( ) -> None: """Check that with live logging enable we are printing the correct headers during start/setup/call/teardown/finish.""" - filename = request.node.name + ".py" + filename = f"{request.node.name}.py" pytester.makeconftest( """ import pytest @@ -421,7 +421,7 @@ def test_sections_single_new_line_after_test_outcome( ) -> None: """Check that only a single new line is written between log messages during teardown/finish.""" - filename = request.node.name + ".py" + filename = f"{request.node.name}.py" pytester.makeconftest( """ import pytest @@ -620,14 +620,14 @@ def test_log_1(): "=* 1 passed in *=", ] ) - assert "INFO" not in stdout else: result.stdout.fnmatch_lines( ["*test_log_cli_auto_enable*100%*", "=* 1 passed in *="] ) - assert "INFO" not in stdout assert "WARNING" not in stdout + assert "INFO" not in stdout + def test_log_file_cli(pytester: Pytester) -> None: # Default log file level @@ -855,7 +855,7 @@ def section(self, *args, **kwargs): handler = _LiveLoggingStreamHandler(out_file, capture_manager) handler.set_when("call") - logger = logging.getLogger(__name__ + ".test_live_logging_suspends_capture") + logger = logging.getLogger(f"{__name__}.test_live_logging_suspends_capture") logger.addHandler(handler) request.addfinalizer(partial(logger.removeHandler, handler)) diff --git a/testing/plugins_integration/simple_integration.py b/testing/plugins_integration/simple_integration.py index 20b2fc4b5bb..50c2222b80d 100644 --- a/testing/plugins_integration/simple_integration.py +++ b/testing/plugins_integration/simple_integration.py @@ -2,9 +2,9 @@ def test_foo(): - assert True + pass @pytest.mark.parametrize("i", range(3)) def test_bar(i): - assert True + pass diff --git a/testing/python/approx.py b/testing/python/approx.py index 6acb466ffb1..3c6b1ff8f02 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -34,14 +34,16 @@ def set_continue(self): monkeypatch.setattr("doctest._OutputRedirectingPdb", MockedPdb) + + class MyDocTestRunner(doctest.DocTestRunner): def report_failure(self, out, test, example, got): raise AssertionError( - "'{}' evaluates to '{}', not '{}'".format( - example.source.strip(), got.strip(), example.want.strip() - ) + f"'{example.source.strip()}' evaluates to '{got.strip()}', not '{example.want.strip()}'" ) + + return MyDocTestRunner() @@ -298,10 +300,10 @@ def test_repr_string(self): assert repr(approx(1.0, rel=inf)) == "1.0 ± inf" # Dictionaries aren't ordered, so we need to check both orders. - assert repr(approx({"a": 1.0, "b": 2.0})) in ( + assert repr(approx({"a": 1.0, "b": 2.0})) in { "approx({'a': 1.0 ± 1.0e-06, 'b': 2.0 ± 2.0e-06})", "approx({'b': 2.0 ± 2.0e-06, 'a': 1.0 ± 1.0e-06})", - ) + } def test_repr_complex_numbers(self): assert repr(approx(inf + 1j)) == "(inf+1j)" @@ -342,9 +344,9 @@ def test_bool(self): def test_operator_overloading(self): assert 1 == approx(1, rel=1e-6, abs=1e-12) - assert not (1 != approx(1, rel=1e-6, abs=1e-12)) + assert approx(1, rel=1e-6, abs=1e-12) == 1 assert 10 != approx(1, rel=1e-6, abs=1e-12) - assert not (10 == approx(1, rel=1e-6, abs=1e-12)) + assert approx(1, rel=1e-6, abs=1e-12) != 10 def test_exactly_equal(self): examples = [ @@ -390,14 +392,14 @@ def test_negative_tolerance( ) -> None: # Negative tolerances are not allowed. with pytest.raises(ValueError): - 1.1 == approx(1, rel, abs) + approx(1, rel, abs) == 1.1 def test_negative_tolerance_message(self): # Error message for negative tolerance should include the value. with pytest.raises(ValueError, match="-3"): - 0 == approx(1, abs=-3) + approx(1, abs=-3) == 0 with pytest.raises(ValueError, match="-3"): - 0 == approx(1, rel=-3) + approx(1, rel=-3) == 0 def test_inf_tolerance(self): # Everything should be equal if the tolerance is infinite. @@ -412,17 +414,17 @@ def test_inf_tolerance_expecting_zero(self) -> None: # If the relative tolerance is zero but the expected value is infinite, # the actual tolerance is a NaN, which should be an error. with pytest.raises(ValueError): - 1 == approx(0, rel=inf, abs=0.0) + approx(0, rel=inf, abs=0.0) == 1 with pytest.raises(ValueError): - 1 == approx(0, rel=inf, abs=inf) + approx(0, rel=inf, abs=inf) == 1 def test_nan_tolerance(self) -> None: with pytest.raises(ValueError): - 1.1 == approx(1, rel=nan) + approx(1, rel=nan) == 1.1 with pytest.raises(ValueError): - 1.1 == approx(1, abs=nan) + approx(1, abs=nan) == 1.1 with pytest.raises(ValueError): - 1.1 == approx(1, rel=nan, abs=nan) + approx(1, rel=nan, abs=nan) == 1.1 def test_reasonable_defaults(self): # Whatever the defaults are, they should work for numbers close to 1 diff --git a/testing/python/collect.py b/testing/python/collect.py index ac3edd395ab..fb460f2e9dc 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -35,9 +35,9 @@ def test_import_duplicate(self, pytester: Pytester) -> None: [ "*import*mismatch*", "*imported*test_whatever*", - "*%s*" % p1, + f"*{p1}*", "*not the same*", - "*%s*" % p2, + f"*{p2}*", "*HINT*", ] ) @@ -537,7 +537,7 @@ def test_function(arg): """ ) assert items[0] != items[1] - assert not (items[0] == items[1]) + assert items[0] != items[1] def test_pyfunc_call(self, pytester: Pytester) -> None: item = pytester.getitem("def test_func(): raise ValueError") @@ -739,7 +739,7 @@ def test_fail(): assert 0 fn3 = pytester.collect_by_name(modcol, "test_fail") assert isinstance(fn3, pytest.Function) - assert not (fn1 == fn3) + assert fn1 != fn3 assert fn1 != fn3 for fn in fn1, fn2, fn3: diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 3ce5cb34ddd..ab2a3c5640c 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -660,7 +660,7 @@ def test_func(something): pass assert req.cls is None assert req.function.__name__ == "test_func" assert req.config == item.config - assert repr(req).find(req.function.__name__) != -1 + assert req.function.__name__ in repr(req) def test_request_attributes_method(self, pytester: Pytester) -> None: (item,) = pytester.getitems( @@ -2903,7 +2903,7 @@ def test_browser(browser): ) ) reprec = pytester.runpytest("-s") - for test in ["test_browser"]: + for _ in ["test_browser"]: reprec.stdout.fnmatch_lines(["*Finalized*"]) def test_class_scope_with_normal_tests(self, pytester: Pytester) -> None: diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 2fed22718b0..6e5abc6e8ac 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -504,9 +504,7 @@ def test_idmaker_idfn(self) -> None: """#351""" def ids(val: object) -> Optional[str]: - if isinstance(val, Exception): - return repr(val) - return None + return repr(val) if isinstance(val, Exception) else None result = IdMaker( ("a", "b"), diff --git a/testing/python/raises.py b/testing/python/raises.py index 3dcec31eb1f..d48d4972113 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -54,7 +54,7 @@ class E(Exception): # this test prints the inflight uninitialized object # using repr and str as well as pprint to demonstrate # it works - print(str(excinfo)) + print(excinfo) print(repr(excinfo)) import pprint @@ -198,7 +198,7 @@ def test_raises_match(self) -> None: f" Regex: {msg!r}\n" " Input: \"invalid literal for int() with base 10: 'asdf'\"" ) - with pytest.raises(AssertionError, match="(?m)" + re.escape(expr)): + with pytest.raises(AssertionError, match=f"(?m){re.escape(expr)}"): with pytest.raises(ValueError, match=msg): int("asdf", base=10) diff --git a/testing/test_argcomplete.py b/testing/test_argcomplete.py index 8c10e230b0c..b7d47e571eb 100644 --- a/testing/test_argcomplete.py +++ b/testing/test_argcomplete.py @@ -48,7 +48,7 @@ def __call__(self, prefix, **kwargs): if self.allowednames: if self.directories: files = _wrapcall(["bash", "-c", f"compgen -A directory -- '{prefix}'"]) - completion += [f + "/" for f in files] + completion += [f"{f}/" for f in files] for x in self.allowednames: completion += _wrapcall( ["bash", "-c", f"compgen -A file -X '!*.{x}' -- '{prefix}'"] @@ -61,7 +61,7 @@ def __call__(self, prefix, **kwargs): completion = list(set(completion) - set(anticomp)) if self.directories: - completion += [f + "/" for f in anticomp] + completion += [f"{f}/" for f in anticomp] return completion diff --git a/testing/test_assertion.py b/testing/test_assertion.py index d8844f2e41d..32fdd2ef1a9 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -18,11 +18,15 @@ def mock_config(verbose=0): + + + class Config: def getoption(self, name): if name == "verbose": return verbose - raise KeyError("Not mocked out: %s" % name) + raise KeyError(f"Not mocked out: {name}") + return Config() @@ -52,7 +56,7 @@ def test(check_first): """, } pytester.makepyfile(**contents) - result = pytester.runpytest_subprocess("--assert=%s" % mode) + result = pytester.runpytest_subprocess(f"--assert={mode}") if mode == "plain": expected = "E AssertionError" elif mode == "rewrite": @@ -114,7 +118,7 @@ def test_foo(check_first): """, } pytester.makepyfile(**contents) - result = pytester.runpytest_subprocess("--assert=%s" % mode) + result = pytester.runpytest_subprocess(f"--assert={mode}") if mode == "plain": expected = "E AssertionError" elif mode == "rewrite": @@ -233,8 +237,9 @@ def test2(check_first2): } pytester.makepyfile(**contents) result = pytester.run( - sys.executable, "mainwrapper.py", "-s", "--assert=%s" % mode + sys.executable, "mainwrapper.py", "-s", f"--assert={mode}" ) + if mode == "plain": expected = "E AssertionError" elif mode == "rewrite": @@ -535,7 +540,7 @@ def test_list_wrap_for_width_rewrap_same_length(self) -> None: def test_list_dont_wrap_strings(self) -> None: long_a = "a" * 10 - l1 = ["a"] + [long_a for _ in range(0, 7)] + l1 = ["a"] + [long_a for _ in range(7)] l2 = ["should not get wrapped"] diff = callequal(l1, l2, verbose=True) assert diff == [ @@ -676,6 +681,7 @@ def test_frozenzet(self) -> None: def test_Sequence(self) -> None: # Test comparing with a Sequence subclass. + class TestSequence(MutableSequence[int]): def __init__(self, iterable): self.elements = list(iterable) @@ -695,7 +701,7 @@ def __delitem__(self, item): def insert(self, item, index): pass - expl = callequal(TestSequence([0, 1]), list([0, 2])) + expl = callequal(TestSequence([0, 1]), [0, 2]) assert expl is not None assert len(expl) > 1 @@ -708,6 +714,7 @@ def test_list_tuples(self) -> None: assert len(expl) > 1 def test_list_bad_repr(self) -> None: + class A: def __repr__(self): raise ValueError(42) @@ -720,10 +727,7 @@ def __repr__(self): assert expl[0].startswith("{} == <[ValueError") assert "raised in repr" in expl[0] assert expl[1:] == [ - "(pytest_assertion plugin: representation of details failed:" - " {}:{}: ValueError: 42.".format( - __file__, A.__repr__.__code__.co_firstlineno + 1 - ), + f"(pytest_assertion plugin: representation of details failed: {__file__}:{A.__repr__.__code__.co_firstlineno + 1}: ValueError: 42.", " Probably an object has a faulty __repr__.)", ] @@ -781,18 +785,10 @@ def test_nfc_nfd_same_string(self) -> None: left = "hyv\xe4" right = "hyva\u0308" expl = callequal(left, right) - assert expl == [ - r"'hyv\xe4' == 'hyva\u0308'", - f"- {str(right)}", - f"+ {str(left)}", - ] + assert expl == [r"'hyv\xe4' == 'hyva\u0308'", f"- {right}", f"+ {left}"] expl = callequal(left, right, verbose=2) - assert expl == [ - r"'hyv\xe4' == 'hyva\u0308'", - f"- {str(right)}", - f"+ {str(left)}", - ] + assert expl == [r"'hyv\xe4' == 'hyva\u0308'", f"- {right}", f"+ {left}"] class TestAssert_reprcompare_dataclass: @@ -1181,12 +1177,12 @@ def test_doesnt_truncate_when_input_is_empty_list(self) -> None: assert result == expl def test_doesnt_truncate_at_when_input_is_5_lines_and_LT_max_chars(self) -> None: - expl = ["a" * 100 for x in range(5)] + expl = ["a" * 100 for _ in range(5)] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) assert result == expl def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None: - expl = ["" for x in range(50)] + expl = ["" for _ in range(50)] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) assert result != expl assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG @@ -1196,7 +1192,7 @@ def test_truncates_at_8_lines_when_given_list_of_empty_strings(self) -> None: assert last_line_before_trunc_msg.endswith("...") def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None: - expl = ["a" for x in range(100)] + expl = ["a" for _ in range(100)] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) assert result != expl assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG @@ -1206,7 +1202,7 @@ def test_truncates_at_8_lines_when_first_8_lines_are_LT_max_chars(self) -> None: assert last_line_before_trunc_msg.endswith("...") def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None: - expl = ["a" * 80 for x in range(16)] + expl = ["a" * 80 for _ in range(16)] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=8 * 80) assert result != expl assert len(result) == 8 + self.LINES_IN_TRUNCATION_MSG @@ -1216,7 +1212,7 @@ def test_truncates_at_8_lines_when_first_8_lines_are_EQ_max_chars(self) -> None: assert last_line_before_trunc_msg.endswith("...") def test_truncates_at_4_lines_when_first_4_lines_are_GT_max_chars(self) -> None: - expl = ["a" * 250 for x in range(10)] + expl = ["a" * 250 for _ in range(10)] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=999) assert result != expl assert len(result) == 4 + self.LINES_IN_TRUNCATION_MSG @@ -1226,7 +1222,7 @@ def test_truncates_at_4_lines_when_first_4_lines_are_GT_max_chars(self) -> None: assert last_line_before_trunc_msg.endswith("...") def test_truncates_at_1_line_when_first_line_is_GT_max_chars(self) -> None: - expl = ["a" * 250 for x in range(1000)] + expl = ["a" * 250 for _ in range(1000)] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) assert result != expl assert len(result) == 1 + self.LINES_IN_TRUNCATION_MSG diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 3c98392ed98..601a479e856 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -52,7 +52,7 @@ def getmsg( code = compile(mod, "", "exec") ns: Dict[str, object] = {} if extra_ns is not None: - ns.update(extra_ns) + ns |= extra_ns exec(code, ns) func = ns[f.__name__] try: @@ -61,9 +61,7 @@ def getmsg( if must_pass: pytest.fail("shouldn't have raised") s = str(sys.exc_info()[1]) - if not s.startswith("assert"): - return "AssertionError: " + s - return s + return s if s.startswith("assert") else f"AssertionError: {s}" else: if not must_pass: pytest.fail("function didn't raise at all") @@ -306,7 +304,7 @@ def test_foo(): result = pytester.runpytest() assert result.ret == 1 result.stdout.fnmatch_lines( - ["*AssertionError*%s*" % repr((1, 2)), "*assert 1 == 2*"] + [f"*AssertionError*{repr((1, 2))}*", "*assert 1 == 2*"] ) def test_assertion_message_expr(self, pytester: Pytester) -> None: @@ -426,7 +424,7 @@ def f1() -> None: def f2() -> None: x = 1 - assert x == 1 or x == 2 + assert x in {1, 2} getmsg(f2, must_pass=True) @@ -869,9 +867,9 @@ def test_foo(): result = pytester.runpytest_subprocess() assert result.ret == 0 found_names = glob.glob(f"__pycache__/*-pytest-{pytest.__version__}.pyc") - assert found_names, "pyc with expected tag not found in names: {}".format( - glob.glob("__pycache__/*.pyc") - ) + assert ( + found_names + ), f'pyc with expected tag not found in names: {glob.glob("__pycache__/*.pyc")}' @pytest.mark.skipif('"__pypy__" in sys.modules') def test_pyc_vs_pyo(self, pytester: Pytester, monkeypatch) -> None: @@ -883,18 +881,18 @@ def test_optimized(): assert test_optimized.__doc__ is None""" ) p = make_numbered_dir(root=Path(pytester.path), prefix="runpytest-") - tmp = "--basetemp=%s" % p + tmp = f"--basetemp={p}" monkeypatch.setenv("PYTHONOPTIMIZE", "2") monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) assert pytester.runpytest_subprocess(tmp).ret == 0 - tagged = "test_pyc_vs_pyo." + PYTEST_TAG - assert tagged + ".pyo" in os.listdir("__pycache__") + tagged = f"test_pyc_vs_pyo.{PYTEST_TAG}" + assert f"{tagged}.pyo" in os.listdir("__pycache__") monkeypatch.undo() monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", raising=False) monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) assert pytester.runpytest_subprocess(tmp).ret == 1 - assert tagged + ".pyc" in os.listdir("__pycache__") + assert f"{tagged}.pyc" in os.listdir("__pycache__") def test_package(self, pytester: Pytester) -> None: pkg = pytester.path.joinpath("pkg") @@ -962,7 +960,7 @@ def test_remember_rewritten_modules( hook.exec_module(module) hook.mark_rewrite("test_remember_rewritten_modules") hook.mark_rewrite("test_remember_rewritten_modules") - assert warnings == [] + assert not warnings def test_rewrite_warning_using_pytest_plugins(self, pytester: Pytester) -> None: pytester.makepyfile( @@ -1064,7 +1062,7 @@ def test_read_pyc(self, tmp_path: Path) -> None: from _pytest.assertion.rewrite import _read_pyc source = tmp_path / "source.py" - pyc = Path(str(source) + "c") + pyc = Path(f"{str(source)}c") source.write_text("def test(): pass") py_compile.compile(str(source), str(pyc)) @@ -1090,7 +1088,7 @@ def test_read_pyc_success(self, tmp_path: Path, pytester: Pytester) -> None: state = AssertionState(config, "rewrite") fn = tmp_path / "source.py" - pyc = Path(str(fn) + "c") + pyc = Path(f"{str(fn)}c") fn.write_text("def test(): assert True") @@ -1732,7 +1730,7 @@ def test_foo(): assert bar_init.is_file() # test file: rewritten, custom pytest cache tag - test_foo_pyc = get_cache_dir(test_foo) / ("test_foo" + PYC_TAIL) + test_foo_pyc = get_cache_dir(test_foo) / f"test_foo{PYC_TAIL}" assert test_foo_pyc.is_file() # normal file: not touched by pytest, normal cache tag diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 2baa3c8f189..47a32ce6cbd 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -186,7 +186,7 @@ def test_cache_reportheader(env, pytester: Pytester, monkeypatch: MonkeyPatch) - monkeypatch.delenv("TOX_ENV_DIR", raising=False) expected = ".pytest_cache" result = pytester.runpytest("-v") - result.stdout.fnmatch_lines(["cachedir: %s" % expected]) + result.stdout.fnmatch_lines([f"cachedir: {expected}"]) def test_cache_reportheader_external_abspath( diff --git a/testing/test_capture.py b/testing/test_capture.py index 00cab19330b..b2550e5fd60 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -110,7 +110,7 @@ def test_unicode(): """ % obj ) - result = pytester.runpytest("--capture=%s" % method) + result = pytester.runpytest(f"--capture={method}") result.stdout.fnmatch_lines(["*1 passed*"]) @@ -122,7 +122,7 @@ def test_unicode(): print('b\\u00f6y') """ ) - result = pytester.runpytest("--capture=%s" % method) + result = pytester.runpytest(f"--capture={method}") result.stdout.fnmatch_lines(["*1 passed*"]) @@ -978,7 +978,7 @@ def test_simple(self, tmpfile: BinaryIO) -> None: assert s == "hello" def test_simple_many(self, tmpfile: BinaryIO) -> None: - for i in range(10): + for _ in range(10): self.test_simple(tmpfile) def test_simple_many_check_open_files(self, pytester: Pytester) -> None: @@ -1241,7 +1241,7 @@ def test_intermingling(self): def test_many(self, capfd): with lsof_check(): - for i in range(10): + for _ in range(10): cap = StdCaptureFD() cap.start_capturing() cap.stop_capturing() diff --git a/testing/test_collection.py b/testing/test_collection.py index 58e1d862a35..82a2f7b5612 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -51,7 +51,7 @@ def test_fail(): assert 0 fn3 = pytester.collect_by_name(modcol, "test_fail") assert isinstance(fn3, pytest.Function) - assert not (fn1 == fn3) + assert fn1 != fn3 assert fn1 != fn3 for fn in fn1, fn2, fn3: @@ -265,14 +265,14 @@ def test_testpaths_ini(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> No # collects the tests for dirname in ("a", "b", "c"): items, reprec = pytester.inline_genitems(tmp_path.joinpath(dirname)) - assert [x.name for x in items] == ["test_%s" % dirname] + assert [x.name for x in items] == [f"test_{dirname}"] # changing cwd to each subdirectory and running pytest without # arguments collects the tests in that directory normally for dirname in ("a", "b", "c"): monkeypatch.chdir(pytester.path.joinpath(dirname)) items, reprec = pytester.inline_genitems() - assert [x.name for x in items] == ["test_%s" % dirname] + assert [x.name for x in items] == [f"test_{dirname}"] class TestCollectPluginHookRelay: @@ -527,8 +527,8 @@ def test_method(self): pass """ ) - normid = p.name + "::TestClass::test_method" - for id in [p.name, p.name + "::TestClass", normid]: + normid = f"{p.name}::TestClass::test_method" + for id in [p.name, f"{p.name}::TestClass", normid]: items, hookrec = pytester.inline_genitems(id) assert len(items) == 1 assert items[0].name == "test_method" @@ -633,7 +633,7 @@ def test_method(self): pass """ ) - arg = p.name + "::TestClass::test_method" + arg = f"{p.name}::TestClass::test_method" items, hookrec = pytester.inline_genitems(arg) assert len(items) == 1 (item,) = items @@ -690,7 +690,7 @@ def test_2(): pass """ ) - shutil.copy(p, p.parent / (p.stem + "2" + ".py")) + shutil.copy(p, p.parent / f"{p.stem}2.py") items, reprec = pytester.inline_genitems(p.parent) assert len(items) == 4 for numi, i in enumerate(items): @@ -798,7 +798,7 @@ def runtest(self): result = pytester.runpytest() assert result.ret == 0 result.stdout.fnmatch_lines(["*2 passed*"]) - res = pytester.runpytest("%s::item2" % p.name) + res = pytester.runpytest(f"{p.name}::item2") res.stdout.fnmatch_lines(["*1 passed*"]) @@ -865,7 +865,7 @@ def test_failing_5(): ) num_matching_tests = 4 for expression in ("specifictopic", "SPECIFICTOPIC", "SpecificTopic"): - reprec = pytester.inline_run("-k " + expression) + reprec = pytester.inline_run(f"-k {expression}") reprec.assertoutcome(passed=num_matching_tests, failed=0) def test_duplicates_handled_correctly(self, pytester: Pytester) -> None: @@ -1266,7 +1266,7 @@ def test_nodeid(request): symlink_to_sub = out_of_tree.joinpath("symlink_to_sub") symlink_or_skip(sub, symlink_to_sub) os.chdir(sub) - result = pytester.runpytest("-vs", "--rootdir=%s" % sub, symlink_to_sub) + result = pytester.runpytest("-vs", f"--rootdir={sub}", symlink_to_sub) result.stdout.fnmatch_lines( [ # Should not contain "sub/"! diff --git a/testing/test_compat.py b/testing/test_compat.py index 8a80fd625dc..fbd5b4902d0 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -216,17 +216,11 @@ def prop(self) -> int: def test_assert_never_union() -> None: x: Union[int, str] = 10 - if isinstance(x, int): - pass - else: + if not isinstance(x, int): with pytest.raises(AssertionError): assert_never(x) # type: ignore[arg-type] - if isinstance(x, int): - pass - elif isinstance(x, str): - pass - else: + if not isinstance(x, int) and not isinstance(x, str): assert_never(x) @@ -234,32 +228,20 @@ def test_assert_never_enum() -> None: E = enum.Enum("E", "a b") x: E = E.a - if x is E.a: - pass - else: + if x is not E.a: with pytest.raises(AssertionError): assert_never(x) # type: ignore[arg-type] - if x is E.a: - pass - elif x is E.b: - pass - else: + if x is not E.a and x is not E.b: assert_never(x) def test_assert_never_literal() -> None: x: Literal["a", "b"] = "a" - if x == "a": - pass - else: + if x != "a": with pytest.raises(AssertionError): assert_never(x) # type: ignore[arg-type] - if x == "a": - pass - elif x == "b": - pass - else: + if x not in ["a", "b"]: assert_never(x) diff --git a/testing/test_config.py b/testing/test_config.py index f5b6d7f9816..2e1e1c0ff5b 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -285,7 +285,7 @@ def pytest_addoption(parser): result = pytester.runpytest("--strict-config") if exception_text: - result.stderr.fnmatch_lines("ERROR: " + exception_text) + result.stderr.fnmatch_lines(f"ERROR: {exception_text}") assert result.ret == pytest.ExitCode.USAGE_ERROR else: result.stderr.no_fnmatch_line(exception_text) @@ -627,11 +627,7 @@ def test_getconftest_pathlist(self, pytester: Pytester, tmp_path: Path) -> None: @pytest.mark.parametrize("maybe_type", ["not passed", "None", '"string"']) def test_addini(self, pytester: Pytester, maybe_type: str) -> None: - if maybe_type == "not passed": - type_string = "" - else: - type_string = f", {maybe_type}" - + type_string = "" if maybe_type == "not passed" else f", {maybe_type}" pytester.makeconftest( f""" def pytest_addoption(parser): @@ -1169,8 +1165,8 @@ def test_invalid_options_show_extra_information(pytester: Pytester) -> None: result.stderr.fnmatch_lines( [ "*error: unrecognized arguments: --invalid-option*", - "* inifile: %s*" % pytester.path.joinpath("tox.ini"), - "* rootdir: %s*" % pytester.path, + f'* inifile: {pytester.path.joinpath("tox.ini")}*', + f"* rootdir: {pytester.path}*", ] ) @@ -1309,7 +1305,7 @@ def exp_match(val: object) -> str: with pytest.raises(pytest.UsageError, match=exp_match({"foo"})): _get_plugin_specs_as_list({"foo"}) # type: ignore[arg-type] with pytest.raises(pytest.UsageError, match=exp_match({})): - _get_plugin_specs_as_list(dict()) # type: ignore[arg-type] + _get_plugin_specs_as_list({}) assert _get_plugin_specs_as_list(None) == [] assert _get_plugin_specs_as_list("") == [] @@ -1652,10 +1648,10 @@ def test_addopts_before_initini( self, monkeypatch: MonkeyPatch, _config_for_test, _sys_snapshot ) -> None: cache_dir = ".custom_cache" - monkeypatch.setenv("PYTEST_ADDOPTS", "-o cache_dir=%s" % cache_dir) + monkeypatch.setenv("PYTEST_ADDOPTS", f"-o cache_dir={cache_dir}") config = _config_for_test config._preparse([], addopts=True) - assert config._override_ini == ["cache_dir=%s" % cache_dir] + assert config._override_ini == [f"cache_dir={cache_dir}"] def test_addopts_from_env_not_concatenated( self, monkeypatch: MonkeyPatch, _config_for_test @@ -1681,10 +1677,10 @@ def test_addopts_from_ini_not_concatenated(self, pytester: Pytester) -> None: result = pytester.runpytest("cache_dir=ignored") result.stderr.fnmatch_lines( [ - "%s: error: argument -o/--override-ini: expected one argument (via addopts config)" - % (pytester._request.config._parser.optparser.prog,) + f"{pytester._request.config._parser.optparser.prog}: error: argument -o/--override-ini: expected one argument (via addopts config)" ] ) + assert result.ret == _pytest.config.ExitCode.USAGE_ERROR def test_override_ini_does_not_contain_paths( @@ -1770,10 +1766,10 @@ def pytest_addoption(parser): result.stderr.fnmatch_lines( [ "ERROR: usage: *", - "%s: error: argument --invalid-option-should-allow-for-help: expected one argument" - % (pytester._request.config._parser.optparser.prog,), + f"{pytester._request.config._parser.optparser.prog}: error: argument --invalid-option-should-allow-for-help: expected one argument", ] ) + # Does not display full/default help. assert "to see available markers type: pytest --markers" not in result.stdout.lines assert result.ret == ExitCode.USAGE_ERROR @@ -1857,7 +1853,7 @@ def test_config_blocked_default_plugins(pytester: Pytester, plugin: str) -> None pytest.skip("does not work with xdist currently") p = pytester.makepyfile("def test(): pass") - result = pytester.runpytest(str(p), "-pno:%s" % plugin) + result = pytester.runpytest(str(p), f"-pno:{plugin}") if plugin == "python": assert result.ret == ExitCode.USAGE_ERROR @@ -1873,7 +1869,7 @@ def test_config_blocked_default_plugins(pytester: Pytester, plugin: str) -> None result.stdout.fnmatch_lines(["* 1 passed in *"]) p = pytester.makepyfile("def test(): assert 0") - result = pytester.runpytest(str(p), "-pno:%s" % plugin) + result = pytester.runpytest(str(p), f"-pno:{plugin}") assert result.ret == ExitCode.TESTS_FAILED if plugin != "terminal": result.stdout.fnmatch_lines(["* 1 failed in *"]) diff --git a/testing/test_conftest.py b/testing/test_conftest.py index d2bf860c6fe..0fe75d86df0 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -228,13 +228,14 @@ def test_setinitial_conftest_subdirs(pytester: Pytester, name: str) -> None: pm = PytestPluginManager() conftest_setinitial(pm, [sub.parent], confcutdir=pytester.path) key = subconftest.resolve() - if name not in ("whatever", ".dotdir"): - assert pm.has_plugin(str(key)) - assert len(set(pm.get_plugins()) - {pm}) == 1 - else: + if name in {"whatever", ".dotdir"}: assert not pm.has_plugin(str(key)) assert len(set(pm.get_plugins()) - {pm}) == 0 + else: + assert pm.has_plugin(str(key)) + assert len(set(pm.get_plugins()) - {pm}) == 1 + def test_conftest_confcutdir(pytester: Pytester) -> None: pytester.makeconftest("assert 0") @@ -247,7 +248,7 @@ def pytest_addoption(parser): """ ) ) - result = pytester.runpytest("-h", "--confcutdir=%s" % x, x) + result = pytester.runpytest("-h", f"--confcutdir={x}", x) result.stdout.fnmatch_lines(["*--xyz*"]) result.stdout.no_fnmatch_line("*warning: could not load initial*") @@ -344,7 +345,7 @@ def fixture(): """ ), } - pytester.makepyfile(**{"real/%s" % k: v for k, v in source.items()}) + pytester.makepyfile(**{f"real/{k}": v for k, v in source.items()}) # Create a build directory that contains symlinks to actual files # but doesn't symlink actual directories. @@ -366,7 +367,7 @@ def test_conftest_badcase(pytester: Pytester) -> None: """Check conftest.py loading when directory casing is wrong (#5792).""" pytester.path.joinpath("JenkinsRoot/test").mkdir(parents=True) source = {"setup.py": "", "test/__init__.py": "", "test/conftest.py": ""} - pytester.makepyfile(**{"JenkinsRoot/%s" % k: v for k, v in source.items()}) + pytester.makepyfile(**{f"JenkinsRoot/{k}": v for k, v in source.items()}) os.chdir(pytester.path.joinpath("jenkinsroot/test")) result = pytester.runpytest() @@ -484,7 +485,7 @@ def pytest_addoption(parser): ) p = sub.joinpath("test_hello.py") p.write_text("def test_hello(): pass") - result = pytester.runpytest(str(p) + "::test_hello", "-h") + result = pytester.runpytest(f"{str(p)}::test_hello", "-h") result.stdout.fnmatch_lines( """ *--hello-world* @@ -554,7 +555,7 @@ def test_no_conftest(fxtr): ) print("created directory structure:") for x in pytester.path.glob("**/"): - print(" " + str(x.relative_to(pytester.path))) + print(f" {str(x.relative_to(pytester.path))}") return {"runner": runner, "package": package, "swc": swc, "snc": snc} @@ -590,9 +591,9 @@ def test_parsefactories_relative_node_ids( ) -> None: """#616""" dirs = self._setup_tree(pytester) - print("pytest run in cwd: %s" % (dirs[chdir].relative_to(pytester.path))) - print("pytestarg : %s" % testarg) - print("expected pass : %s" % expect_ntests_passed) + print(f"pytest run in cwd: {dirs[chdir].relative_to(pytester.path)}") + print(f"pytestarg : {testarg}") + print(f"expected pass : {expect_ntests_passed}") os.chdir(dirs[chdir]) reprec = pytester.inline_run(testarg, "-q", "--traceconfig") reprec.assertoutcome(passed=expect_ntests_passed) @@ -642,7 +643,7 @@ def out_of_reach(): pass args = [str(src)] if confcutdir: - args = ["--confcutdir=%s" % root.joinpath(confcutdir)] + args = [f"--confcutdir={root.joinpath(confcutdir)}"] result = pytester.runpytest(*args) match = "" if passed: diff --git a/testing/test_debugging.py b/testing/test_debugging.py index 08ae09658ac..3b7edce3ff0 100644 --- a/testing/test_debugging.py +++ b/testing/test_debugging.py @@ -205,7 +205,7 @@ def test_not_called_due_to_quit(): pass """ ) - child = pytester.spawn_pytest("--pdb %s" % p1) + child = pytester.spawn_pytest(f"--pdb {p1}") child.expect("captured stdout") child.expect("get rekt") child.expect("captured stderr") @@ -230,7 +230,7 @@ def test_1(): assert False """ ) - child = pytester.spawn_pytest("--pdb %s" % p1) + child = pytester.spawn_pytest(f"--pdb {p1}") child.expect("Pdb") output = child.before.decode("utf8") child.sendeof() @@ -249,7 +249,7 @@ def test_1(): """ ) child = pytester.spawn_pytest(f"--show-capture={showcapture} --pdb {p1}") - if showcapture in ("all", "log"): + if showcapture in {"all", "log"}: child.expect("captured log") child.expect("get rekt") child.expect("Pdb") @@ -267,7 +267,7 @@ def test_1(): assert False """ ) - child = pytester.spawn_pytest("--show-capture=all --pdb -p no:logging %s" % p1) + child = pytester.spawn_pytest(f"--show-capture=all --pdb -p no:logging {p1}") child.expect("get rekt") output = child.before.decode("utf8") assert "captured log" not in output @@ -287,7 +287,7 @@ def test_1(): pytest.raises(ValueError, globalfunc) """ ) - child = pytester.spawn_pytest("--pdb %s" % p1) + child = pytester.spawn_pytest(f"--pdb {p1}") child.expect(".*def test_1") child.expect(".*pytest.raises.*globalfunc") child.expect("Pdb") @@ -304,7 +304,7 @@ def test_pdb_interaction_on_collection_issue181(self, pytester: Pytester) -> Non xxx """ ) - child = pytester.spawn_pytest("--pdb %s" % p1) + child = pytester.spawn_pytest(f"--pdb {p1}") # child.expect(".*import pytest.*") child.expect("Pdb") child.sendline("c") @@ -319,7 +319,7 @@ def pytest_runtest_protocol(): """ ) p1 = pytester.makepyfile("def test_func(): pass") - child = pytester.spawn_pytest("--pdb %s" % p1) + child = pytester.spawn_pytest(f"--pdb {p1}") child.expect("Pdb") # INTERNALERROR is only displayed once via terminal reporter. @@ -445,7 +445,7 @@ def test_1(capsys, caplog): assert 0 """ ) - child = pytester.spawn_pytest("--pdb %s" % str(p1)) + child = pytester.spawn_pytest(f"--pdb {str(p1)}") child.send("caplog.record_tuples\n") child.expect_exact( "[('test_pdb_with_caplog_on_pdb_invocation', 30, 'some_warning')]" @@ -485,7 +485,7 @@ def function_1(): ''' """ ) - child = pytester.spawn_pytest("--doctest-modules --pdb %s" % p1) + child = pytester.spawn_pytest(f"--doctest-modules --pdb {p1}") child.expect("Pdb") assert "UNEXPECTED EXCEPTION: AssertionError()" in child.before.decode("utf8") @@ -512,7 +512,7 @@ def function_1(): ) # NOTE: does not use pytest.set_trace, but Python's patched pdb, # therefore "-s" is required. - child = pytester.spawn_pytest("--doctest-modules --pdb -s %s" % p1) + child = pytester.spawn_pytest(f"--doctest-modules --pdb -s {p1}") child.expect("Pdb") child.sendline("q") rest = child.read().decode("utf8") @@ -605,7 +605,7 @@ def test_1(): pytest.fail("expected_failure") """ ) - child = pytester.spawn_pytest("--pdbcls=mytest:CustomPdb %s" % str(p1)) + child = pytester.spawn_pytest(f"--pdbcls=mytest:CustomPdb {str(p1)}") child.expect(r"PDB set_trace \(IO-capturing turned off\)") child.expect(r"\n\(Pdb") child.sendline("debug foo()") @@ -642,7 +642,7 @@ def test_1(): pytest.set_trace() """ ) - child = pytester.spawn_pytest("-s %s" % p1) + child = pytester.spawn_pytest(f"-s {p1}") child.expect(r">>> PDB set_trace >>>") child.expect("Pdb") child.sendline("c") @@ -822,9 +822,9 @@ def test_post_mortem(): """ ) if post_mortem: - child = pytester.spawn_pytest(str(p1) + " --pdb -s -k test_post_mortem") + child = pytester.spawn_pytest(f"{str(p1)} --pdb -s -k test_post_mortem") else: - child = pytester.spawn_pytest(str(p1) + " -k test_set_trace") + child = pytester.spawn_pytest(f"{str(p1)} -k test_set_trace") child.expect("enter_pdb_hook") child.sendline("c") if post_mortem: @@ -869,7 +869,7 @@ def test_pdb_custom_cls_without_pdb( p1 = pytester.makepyfile("""xxx """) result = pytester.runpytest_inprocess("--pdbcls=_pytest:_CustomPdb", p1) result.stdout.fnmatch_lines(["*NameError*xxx*", "*1 error*"]) - assert custom_pdb_calls == [] + assert not custom_pdb_calls def test_pdb_custom_cls_with_set_trace( self, @@ -898,7 +898,7 @@ def test_foo(): """ ) monkeypatch.setenv("PYTHONPATH", str(pytester.path)) - child = pytester.spawn_pytest("--pdbcls=custom_pdb:CustomPdb %s" % str(p1)) + child = pytester.spawn_pytest(f"--pdbcls=custom_pdb:CustomPdb {str(p1)}") child.expect("__init__") child.expect("custom set_trace>") @@ -983,10 +983,7 @@ def test_nothing(): pass result = pytester.runpytest_subprocess(*args) result.stdout.fnmatch_lines(["*1 passed in *"]) - @pytest.mark.skipif( - not _ENVIRON_PYTHONBREAKPOINT == "", - reason="Requires breakpoint() default value", - ) + @pytest.mark.skipif(_ENVIRON_PYTHONBREAKPOINT != "", reason="Requires breakpoint() default value") def test_sys_breakpoint_interception(self, pytester: Pytester) -> None: p1 = pytester.makepyfile( """ @@ -1037,7 +1034,7 @@ def test_3(): pass """ ) - child = pytester.spawn_pytest("--trace " + str(p1)) + child = pytester.spawn_pytest(f"--trace {str(p1)}") child.expect("test_1") child.expect("Pdb") child.sendline("c") @@ -1076,7 +1073,7 @@ def test_func_kw(myparam, request, func="func_kw"): assert request.function.__name__ == "test_func_kw" """ ) - child = pytester.spawn_pytest("--trace " + str(p1)) + child = pytester.spawn_pytest(f"--trace {str(p1)}") for func, argname in [ ("test_1", "myparam"), ("test_func", "func"), @@ -1187,15 +1184,16 @@ def test_inner({fixture}): ) ) - child = pytester.spawn_pytest(str(p1) + " -s") + child = pytester.spawn_pytest(f"{str(p1)} -s") child.expect("Pdb") before = child.before.decode("utf8") assert ( - "> PDB set_trace (IO-capturing turned off for fixture %s) >" % (fixture) + f"> PDB set_trace (IO-capturing turned off for fixture {fixture}) >" in before ) + # Test that capturing is really suspended. child.sendline("p 40 + 2") child.expect("Pdb") @@ -1209,7 +1207,7 @@ def test_inner({fixture}): TestPDB.flush(child) assert child.exitstatus == 0 assert "= 1 passed in" in rest - assert "> PDB continue (IO-capturing resumed for fixture %s) >" % (fixture) in rest + assert f"> PDB continue (IO-capturing resumed for fixture {fixture}) >" in rest def test_pdbcls_via_local_module(pytester: Pytester) -> None: diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 2f73feb8c4b..3bd7cb81e12 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1582,10 +1582,8 @@ def test_is_setup_py_is_a_setup_py(tmp_path: Path, mod: str) -> None: @pytest.mark.parametrize("mod", ("setuptools", "distutils.core")) def test_is_setup_py_different_encoding(tmp_path: Path, mod: str) -> None: setup_py = tmp_path.joinpath("setup.py") - contents = ( - "# -*- coding: cp1252 -*-\n" - 'from {} import setup; setup(name="foo", description="€")\n'.format(mod) - ) + contents = f'# -*- coding: cp1252 -*-\nfrom {mod} import setup; setup(name="foo", description="€")\n' + setup_py.write_bytes(contents.encode("cp1252")) assert _is_setup_py(setup_py) diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index 5b7911f21f8..0aed5fa1fbf 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -95,12 +95,12 @@ def test_timeout(): faulthandler_timeout = 0.01 """ ) - args = ["-p", "no:faulthandler"] if not enabled else [] + args = [] if enabled else ["-p", "no:faulthandler"] result = pytester.runpytest_subprocess(*args) tb_output = "most recent call first" if enabled: - result.stderr.fnmatch_lines(["*%s*" % tb_output]) + result.stderr.fnmatch_lines([f"*{tb_output}*"]) else: assert tb_output not in result.stderr.str() result.stdout.fnmatch_lines(["*1 passed*"]) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index b266c76d922..710e1ca3d7f 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -41,9 +41,9 @@ def __call__( self, *args: Union[str, "os.PathLike[str]"], family: Optional[str] = "xunit1" ) -> Tuple[RunResult, "DomNode"]: if family: - args = ("-o", "junit_family=" + family) + args + args = ("-o", f"junit_family={family}") + args xml_path = self.pytester.path.joinpath("junit.xml") - result = self.pytester.runpytest("--junitxml=%s" % xml_path, *args) + result = self.pytester.runpytest(f"--junitxml={xml_path}", *args) if family == "xunit2": with xml_path.open() as f: self.schema.validate(f) @@ -520,8 +520,9 @@ def test_fail(): ) result, dom = run_and_parse( - "-o", "junit_logging=%s" % junit_logging, family=xunit_family + "-o", f"junit_logging={junit_logging}", family=xunit_family ) + assert result.ret, "Expected ret > 0" node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=1, tests=1) @@ -605,9 +606,7 @@ def test_func(arg1): for index, char in enumerate("<&'"): tnode = node.find_nth_by_tag("testcase", index) - tnode.assert_attr( - classname="test_failure_escape", name="test_func[%s]" % char - ) + tnode.assert_attr(classname="test_failure_escape", name=f"test_func[{char}]") sysout = tnode.find_first_by_tag("system-out") text = sysout.text assert "%s\n" % char in text @@ -695,15 +694,15 @@ def test_fail(): assert 0 """ ) - result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging) + result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") - if junit_logging in ["system-err", "out-err", "all"]: + if junit_logging in {"system-err", "out-err", "all"}: assert len(tnode.find_by_tag("system-err")) == 1 else: assert len(tnode.find_by_tag("system-err")) == 0 - if junit_logging in ["log", "system-out", "out-err", "all"]: + if junit_logging in {"log", "system-out", "out-err", "all"}: assert len(tnode.find_by_tag("system-out")) == 1 else: assert len(tnode.find_by_tag("system-out")) == 0 @@ -806,14 +805,14 @@ def test_pass(): print('hello-stdout') """ ) - result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging) + result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") if junit_logging == "no": assert not node.find_by_tag( "system-out" ), "system-out should not be generated" - if junit_logging == "system-out": + elif junit_logging == "system-out": systemout = pnode.find_first_by_tag("system-out") assert ( "hello-stdout" in systemout.toxml() @@ -830,14 +829,14 @@ def test_pass(): sys.stderr.write('hello-stderr') """ ) - result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging) + result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") if junit_logging == "no": assert not node.find_by_tag( "system-err" ), "system-err should not be generated" - if junit_logging == "system-err": + elif junit_logging == "system-err": systemerr = pnode.find_first_by_tag("system-err") assert ( "hello-stderr" in systemerr.toxml() @@ -859,14 +858,14 @@ def test_function(arg): pass """ ) - result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging) + result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") if junit_logging == "no": assert not node.find_by_tag( "system-out" ), "system-out should not be generated" - if junit_logging == "system-out": + elif junit_logging == "system-out": systemout = pnode.find_first_by_tag("system-out") assert ( "hello-stdout" in systemout.toxml() @@ -889,14 +888,14 @@ def test_function(arg): pass """ ) - result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging) + result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") if junit_logging == "no": assert not node.find_by_tag( "system-err" ), "system-err should not be generated" - if junit_logging == "system-err": + elif junit_logging == "system-err": systemerr = pnode.find_first_by_tag("system-err") assert ( "hello-stderr" in systemerr.toxml() @@ -920,14 +919,14 @@ def test_function(arg): sys.stdout.write('hello-stdout call') """ ) - result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging) + result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") if junit_logging == "no": assert not node.find_by_tag( "system-out" ), "system-out should not be generated" - if junit_logging == "system-out": + elif junit_logging == "system-out": systemout = pnode.find_first_by_tag("system-out") assert "hello-stdout call" in systemout.toxml() assert "hello-stdout teardown" in systemout.toxml() @@ -1014,13 +1013,16 @@ def test_print_nullbyte(): """ ) xmlf = pytester.path.joinpath("junit.xml") - pytester.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging) + pytester.runpytest( + f"--junitxml={xmlf}", "-o", f"junit_logging={junit_logging}" + ) + text = xmlf.read_text() assert "\x00" not in text - if junit_logging == "system-out": - assert "#x00" in text if junit_logging == "no": assert "#x00" not in text + elif junit_logging == "system-out": + assert "#x00" in text @pytest.mark.parametrize("junit_logging", ["no", "system-out"]) @@ -1036,12 +1038,15 @@ def test_print_nullbyte(): """ ) xmlf = pytester.path.joinpath("junit.xml") - pytester.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging) + pytester.runpytest( + f"--junitxml={xmlf}", "-o", f"junit_logging={junit_logging}" + ) + text = xmlf.read_text() - if junit_logging == "system-out": - assert "#x0" in text if junit_logging == "no": assert "#x0" not in text + elif junit_logging == "system-out": + assert "#x0" in text def test_invalid_xml_escape() -> None: @@ -1071,10 +1076,7 @@ def test_invalid_xml_escape() -> None: for i in invalid: got = bin_xml_escape(chr(i)) - if i <= 0xFF: - expected = "#x%02X" % i - else: - expected = "#x%04X" % i + expected = "#x%02X" % i if i <= 0xFF else "#x%04X" % i assert got == expected for i in valid: assert chr(i) == bin_xml_escape(chr(i)) @@ -1353,10 +1355,11 @@ def test_x(i): ) _, dom = run_and_parse("-n2") suite_node = dom.find_first_by_tag("testsuite") - failed = [] - for case_node in suite_node.find_by_tag("testcase"): - if case_node.find_first_by_tag("failure"): - failed.append(case_node["name"]) + failed = [ + case_node["name"] + for case_node in suite_node.find_by_tag("testcase") + if case_node.find_first_by_tag("failure") + ] assert failed == ["test_x[22]"] @@ -1709,16 +1712,17 @@ def test_func(): """ ) result, dom = run_and_parse( - "-o", "junit_logging=%s" % junit_logging, family=xunit_family + "-o", f"junit_logging={junit_logging}", family=xunit_family ) + assert result.ret == 1 node = dom.find_first_by_tag("testcase") - if junit_logging == "system-out": - assert len(node.find_by_tag("system-err")) == 0 - assert len(node.find_by_tag("system-out")) == 1 - elif junit_logging == "system-err": + if junit_logging == "system-err": assert len(node.find_by_tag("system-err")) == 1 assert len(node.find_by_tag("system-out")) == 0 + elif junit_logging == "system-out": + assert len(node.find_by_tag("system-err")) == 0 + assert len(node.find_by_tag("system-out")) == 1 else: assert junit_logging == "no" assert len(node.find_by_tag("system-err")) == 0 diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py index fbfd88b7384..fce7aac3285 100644 --- a/testing/test_legacypath.py +++ b/testing/test_legacypath.py @@ -77,7 +77,7 @@ def test_1(tmpdir): assert os.path.realpath(str(tmpdir)) == str(tmpdir) """ ) - result = pytester.runpytest("-s", p, "--basetemp=%s/bt" % linktemp) + result = pytester.runpytest("-s", p, f"--basetemp={linktemp}/bt") assert not result.ret diff --git a/testing/test_link_resolve.py b/testing/test_link_resolve.py index 60a86ada36e..cdbc701738d 100644 --- a/testing/test_link_resolve.py +++ b/testing/test_link_resolve.py @@ -26,8 +26,7 @@ def subst_path_windows(filepath: Path): subprocess.check_call(args) assert os.path.exists(drive) try: - filename = Path(drive, os.sep, basename) - yield filename + yield Path(drive, os.sep, basename) finally: args = ["subst", "/D", drive] subprocess.check_call(args) @@ -41,8 +40,7 @@ def subst_path_linux(filepath: Path): target = directory / ".." / "sub2" os.symlink(str(directory), str(target), target_is_directory=True) try: - filename = target / basename - yield filename + yield target / basename finally: # We don't need to unlink (it's all in the tempdir). pass @@ -62,10 +60,7 @@ def test_foo(): ) ) - subst = subst_path_linux - if sys.platform == "win32": - subst = subst_path_windows - + subst = subst_path_windows if sys.platform == "win32" else subst_path_linux with subst(p) as subst_p: result = pytester.runpytest(str(subst_p), "-v") # i.e.: Make sure that the error is reported as a relative path, not as a diff --git a/testing/test_mark.py b/testing/test_mark.py index 65f2581bd63..319a916f682 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -340,7 +340,7 @@ def test_func(arg): ) rec = pytester.inline_run() passed, skipped, fail = rec.listoutcomes() - expected_id = "test_func[" + pytest.__name__ + "]" + expected_id = f"test_func[{pytest.__name__}]" assert passed[0].nodeid.split("::")[-1] == expected_id @@ -399,7 +399,7 @@ def test_func(arg): """ ) file_name = os.path.basename(py_file) - rec = pytester.inline_run(file_name + "::" + "test_func") + rec = pytester.inline_run(f"{file_name}::test_func") rec.assertoutcome(passed=3) diff --git a/testing/test_meta.py b/testing/test_meta.py index 9201bd21611..0363b5bfa0b 100644 --- a/testing/test_meta.py +++ b/testing/test_meta.py @@ -16,7 +16,9 @@ def _modules() -> List[str]: pytest_pkg: str = _pytest.__path__ # type: ignore return sorted( n - for _, n, _ in pkgutil.walk_packages(pytest_pkg, prefix=_pytest.__name__ + ".") + for _, n, _ in pkgutil.walk_packages( + pytest_pkg, prefix=f"{_pytest.__name__}." + ) ) diff --git a/testing/test_nodes.py b/testing/test_nodes.py index df1439e1c49..0570670043e 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -164,4 +164,4 @@ def test_show_wrong_path(private_dir): """ ) result = pytester.runpytest() - result.stdout.fnmatch_lines([str(p) + ":*: AssertionError", "*1 failed in *"]) + result.stdout.fnmatch_lines([f"{str(p)}:*: AssertionError", "*1 failed in *"]) diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 992f49bc53c..1b647f1d020 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -305,15 +305,14 @@ def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: script = str(pytester.path.joinpath("test_argcomplete")) - with open(str(script), "w") as fp: + with open(script, "w") as fp: # redirect output from argcomplete to stdin and stderr is not trivial # http://stackoverflow.com/q/12589419/1307905 # so we use bash fp.write( - 'COMP_WORDBREAKS="$COMP_WORDBREAKS" {} -m pytest 8>&1 9>&2'.format( - shlex.quote(sys.executable) - ) + f'COMP_WORDBREAKS="$COMP_WORDBREAKS" {shlex.quote(sys.executable)} -m pytest 8>&1 9>&2' ) + # alternative would be extended Pytester.{run(),_run(),popen()} to be able # to handle a keyword argument env that replaces os.environ in popen or # extends the copy, advantage: could not forget to restore @@ -322,9 +321,9 @@ def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: monkeypatch.setenv("COMP_WORDBREAKS", " \\t\\n\"\\'><=;|&(:") arg = "--fu" - monkeypatch.setenv("COMP_LINE", "pytest " + arg) - monkeypatch.setenv("COMP_POINT", str(len("pytest " + arg))) - result = pytester.run("bash", str(script), arg) + monkeypatch.setenv("COMP_LINE", f"pytest {arg}") + monkeypatch.setenv("COMP_POINT", str(len(f"pytest {arg}"))) + result = pytester.run("bash", script, arg) if result.ret == 255: # argcomplete not found pytest.skip("argcomplete not available") @@ -338,7 +337,7 @@ def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: result.stdout.fnmatch_lines(["--funcargs", "--fulltrace"]) os.mkdir("test_argcomplete.d") arg = "test_argc" - monkeypatch.setenv("COMP_LINE", "pytest " + arg) - monkeypatch.setenv("COMP_POINT", str(len("pytest " + arg))) - result = pytester.run("bash", str(script), arg) + monkeypatch.setenv("COMP_LINE", f"pytest {arg}") + monkeypatch.setenv("COMP_POINT", str(len(f"pytest {arg}"))) + result = pytester.run("bash", script, arg) result.stdout.fnmatch_lines(["test_argcomplete", "test_argcomplete.d/"]) diff --git a/testing/test_pastebin.py b/testing/test_pastebin.py index 86b231f8b5e..b528e25c3d3 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -170,7 +170,7 @@ def test_create_new_paste(self, pastebin, mocked_urlopen) -> None: assert type(data) is bytes lexer = "text" assert url == "https://bpa.st" - assert "lexer=%s" % lexer in data.decode() + assert f"lexer={lexer}" in data.decode() assert "code=full-paste-contents" in data.decode() assert "expiry=1week" in data.decode() diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index c901dc6f435..55b680431f3 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -221,7 +221,7 @@ def test_check_filepath_consistency( self, monkeypatch: MonkeyPatch, tmp_path: Path ) -> None: name = "pointsback123" - p = tmp_path.joinpath(name + ".py") + p = tmp_path.joinpath(f"{name}.py") p.touch() for ending in (".pyc", ".pyo"): mod = ModuleType(name) @@ -233,7 +233,7 @@ def test_check_filepath_consistency( assert mod == newmod monkeypatch.undo() mod = ModuleType(name) - pseudopath = tmp_path.joinpath(name + "123.py") + pseudopath = tmp_path.joinpath(f"{name}123.py") pseudopath.touch() mod.__file__ = str(pseudopath) monkeypatch.setitem(sys.modules, name, mod) @@ -408,8 +408,8 @@ def test_suppress_error_removing_lock(tmp_path: Path) -> None: def test_bestrelpath() -> None: curdir = Path("/foo/bar/baz/path") assert bestrelpath(curdir, curdir) == "." - assert bestrelpath(curdir, curdir / "hello" / "world") == "hello" + os.sep + "world" - assert bestrelpath(curdir, curdir.parent / "sister") == ".." + os.sep + "sister" + assert bestrelpath(curdir, curdir / "hello" / "world") == f"hello{os.sep}world" + assert bestrelpath(curdir, curdir.parent / "sister") == f"..{os.sep}sister" assert bestrelpath(curdir, curdir.parent) == ".." assert bestrelpath(curdir, Path("hello")) == "hello" @@ -419,7 +419,7 @@ def test_commonpath() -> None: subpath = path / "sampledir" assert commonpath(path, subpath) == path assert commonpath(subpath, path) == path - assert commonpath(Path(str(path) + "suffix"), path) == path.parent + assert commonpath(Path(f"{str(path)}suffix"), path) == path.parent assert commonpath(path, path.parent.parent) == path.parent.parent @@ -579,4 +579,4 @@ def test_insert_missing_modules( modules = {} insert_missing_modules(modules, "") - assert modules == {} + assert not modules diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 9fe23d17792..738b9b3676e 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -87,7 +87,7 @@ def pytest_configure(self): values.append(self) config.pluginmanager.register(A()) - assert len(values) == 0 + assert not values config._do_configure() assert len(values) == 1 config.pluginmanager.register(A()) # leads to a configured() plugin diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 62dad98589d..0777666279c 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -24,9 +24,7 @@ def test_make_hook_recorder(pytester: Pytester) -> None: recorder = pytester.make_hook_recorder(item.config.pluginmanager) assert not recorder.getfailures() - # (The silly condition is to fool mypy that the code below this is reachable) - if 1 + 1 == 2: - pytest.xfail("internal reportrecorder tests need refactoring") + pytest.xfail("internal reportrecorder tests need refactoring") class rep: excinfo = None @@ -347,9 +345,9 @@ def test_restore_reloaded(self, monkeypatch: MonkeyPatch) -> None: def test_preserve_modules(self, monkeypatch: MonkeyPatch) -> None: key = [self.key + str(i) for i in range(3)] - assert not any(k in sys.modules for k in key) + assert all(k not in sys.modules for k in key) for i, k in enumerate(key): - mod = ModuleType("something" + str(i)) + mod = ModuleType(f"something{str(i)}") monkeypatch.setitem(sys.modules, k, mod) original = dict(sys.modules) @@ -381,7 +379,7 @@ class TestSysPathsSnapshot: @staticmethod def path(n: int) -> str: - return "my-dirty-little-secret-" + str(n) + return f"my-dirty-little-secret-{n}" def test_restore(self, monkeypatch: MonkeyPatch, path_type) -> None: other_path_type = self.other_path[path_type] @@ -576,7 +574,7 @@ def test_linematcher_no_matching(function: str) -> None: ) # check the function twice to ensure we don't accumulate the internal buffer - for i in range(2): + for _ in range(2): with pytest.raises(pytest.fail.Exception) as e: func = getattr(lm, function) func(good_pattern) diff --git a/testing/test_python_path.py b/testing/test_python_path.py index 5ee0f55e36a..6ce4f9e2e16 100644 --- a/testing/test_python_path.py +++ b/testing/test_python_path.py @@ -107,4 +107,4 @@ def pytest_unconfigure(self) -> Generator[None, None, None]: assert before is not None assert after is not None assert any("I_SHALL_BE_REMOVED" in entry for entry in before) - assert not any("I_SHALL_BE_REMOVED" in entry for entry in after) + assert all("I_SHALL_BE_REMOVED" not in entry for entry in after) diff --git a/testing/test_runner.py b/testing/test_runner.py index 2e2c462d978..92ae3c13251 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -481,7 +481,7 @@ def test_callinfo() -> None: assert repr(ci) == "" assert str(ci) == "" - ci2 = runner.CallInfo.from_call(lambda: 0 / 0, "collect") + ci2 = runner.CallInfo.from_call(lambda: 1, "collect") assert ci2.when == "collect" assert not hasattr(ci2, "result") assert repr(ci2) == f"" @@ -932,10 +932,11 @@ def test(fix): assert result.ret == 0 test_id = "test_current_test_env_var.py::test" assert pytest_current_test_vars == [ - ("setup", test_id + " (setup)"), - ("call", test_id + " (call)"), - ("teardown", test_id + " (teardown)"), + ("setup", f"{test_id} (setup)"), + ("call", f"{test_id} (call)"), + ("teardown", f"{test_id} (teardown)"), ] + assert "PYTEST_CURRENT_TEST" not in os.environ diff --git a/testing/test_session.py b/testing/test_session.py index f73dc89ef33..7553f6d5564 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -53,7 +53,7 @@ def test_this(): values = reprec.getfailedcollections() assert len(values) == 1 out = str(values[0].longrepr) - assert out.find("does_not_work") != -1 + assert "does_not_work" in out def test_raises_output(self, pytester: Pytester) -> None: reprec = pytester.inline_runsource( @@ -73,7 +73,7 @@ def test_syntax_error_module(self, pytester: Pytester) -> None: values = reprec.getfailedcollections() assert len(values) == 1 out = str(values[0].longrepr) - assert out.find("not python") != -1 + assert "not python" in out def test_exit_first_problem(self, pytester: Pytester) -> None: reprec = pytester.inline_runsource( diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 6415480ef4f..8855937f3b8 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -73,7 +73,7 @@ def test_marked_one_arg_twice(self, pytester: Pytester) -> None: """@pytest.mark.skipif("not hasattr(os, 'murks')")""", """@pytest.mark.skipif(condition="hasattr(os, 'murks')")""", ] - for i in range(0, 2): + for i in range(2): item = pytester.getitem( """ import pytest diff --git a/testing/test_stash.py b/testing/test_stash.py index 2c9df4832e4..ba2dd08bf68 100644 --- a/testing/test_stash.py +++ b/testing/test_stash.py @@ -21,7 +21,7 @@ def test_stash() -> None: stash[key1] = "world" assert stash[key1] == "world" # Has correct type (no mypy error). - stash[key1] + "string" + f"{stash[key1]}string" assert len(stash) == 1 assert stash diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 9a8dd4d9ac9..38d654c677e 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -44,9 +44,7 @@ def __init__(self, verbosity=0): @property def args(self): - values = [] - values.append("--verbosity=%d" % self.verbosity) - return values + return ["--verbosity=%d" % self.verbosity] @pytest.fixture( @@ -123,7 +121,7 @@ def test_writeline(self, pytester: Pytester, linecomp) -> None: rep.write_line("hello world") lines = linecomp.stringio.getvalue().split("\n") assert not lines[0] - assert lines[1].endswith(modcol.name + " .") + assert lines[1].endswith(f"{modcol.name} .") assert lines[2] == "hello world" def test_show_runtest_logstart(self, pytester: Pytester, linecomp) -> None: @@ -860,17 +858,12 @@ def test_passes(): result.stdout.fnmatch_lines( [ "*===== test session starts ====*", - "platform %s -- Python %s*pytest-%s**pluggy-%s" - % ( - sys.platform, - verinfo, - pytest.__version__, - pluggy.__version__, - ), + f"platform {sys.platform} -- Python {verinfo}*pytest-{pytest.__version__}**pluggy-{pluggy.__version__}", "*test_header_trailer_info.py .*", "=* 1 passed*in *.[0-9][0-9]s *=", ] ) + if request.config.pluginmanager.list_plugin_distinfo(): result.stdout.fnmatch_lines(["plugins: *"]) @@ -887,14 +880,9 @@ def test_passes(): result = pytester.runpytest("--no-header") verinfo = ".".join(map(str, sys.version_info[:3])) result.stdout.no_fnmatch_line( - "platform %s -- Python %s*pytest-%s**pluggy-%s" - % ( - sys.platform, - verinfo, - pytest.__version__, - pluggy.__version__, - ) + f"platform {sys.platform} -- Python {verinfo}*pytest-{pytest.__version__}**pluggy-{pluggy.__version__}" ) + if request.config.pluginmanager.list_plugin_distinfo(): result.stdout.no_fnmatch_line("plugins: *") @@ -944,9 +932,7 @@ def test_header_absolute_testpath( result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "rootdir: *absolute_testpath0, configfile: pyproject.toml, testpaths: {}".format( - tests - ) + f"rootdir: *absolute_testpath0, configfile: pyproject.toml, testpaths: {tests}" ] ) @@ -1411,7 +1397,7 @@ def test_opt(arg): s = result.stdout.str() assert "arg = 42" not in s assert "x = 0" not in s - result.stdout.fnmatch_lines(["*%s:8*" % p.name, " assert x", "E assert*"]) + result.stdout.fnmatch_lines([f"*{p.name}:8*", " assert x", "E assert*"]) result = pytester.runpytest() s = result.stdout.str() assert "x = 0" in s @@ -1487,8 +1473,8 @@ def test_func(): """ ) for tbopt in ["long", "short", "no"]: - print("testing --tb=%s..." % tbopt) - result = pytester.runpytest("-rN", "--tb=%s" % tbopt) + print(f"testing --tb={tbopt}...") + result = pytester.runpytest("-rN", f"--tb={tbopt}") s = result.stdout.str() if tbopt == "long": assert "print(6*7)" in s @@ -1518,8 +1504,9 @@ def test_func2(): result = pytester.runpytest("--tb=line") bn = p.name result.stdout.fnmatch_lines( - ["*%s:3: IndexError*" % bn, "*%s:8: AssertionError: hello*" % bn] + [f"*{bn}:3: IndexError*", f"*{bn}:8: AssertionError: hello*"] ) + s = result.stdout.str() assert "def test_func2" not in s @@ -1911,7 +1898,6 @@ def test_summary_stats( ) -> None: tr.stats = stats_arg - # Fake "_is_last_item" to be True. class fake_session: testscollected = 0 @@ -1921,7 +1907,7 @@ class fake_session: # Reset cache. tr._main_color = None - print("Based on stats: %s" % stats_arg) + print(f"Based on stats: {stats_arg}") print(f'Expect summary: "{exp_line}"; with color "{exp_color}"') (line, color) = tr.build_summary_stats_line() print(f'Actually got: "{line}"; with color "{color}"') diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 4f7c5384700..1a018aa9bf7 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -76,11 +76,11 @@ def test_1(tmp_path): pass """ ) - pytester.runpytest(p, "--basetemp=%s" % mytemp) + pytester.runpytest(p, f"--basetemp={mytemp}") assert mytemp.exists() mytemp.joinpath("hello").touch() - pytester.runpytest(p, "--basetemp=%s" % mytemp) + pytester.runpytest(p, f"--basetemp={mytemp}") assert mytemp.exists() assert not mytemp.joinpath("hello").exists() @@ -109,7 +109,7 @@ def test_abs_path(tmp_path_factory): ) ) - result = pytester.runpytest(p, "--basetemp=%s" % mytemp) + result = pytester.runpytest(p, f"--basetemp={mytemp}") if is_ok: assert result.ret == 0 assert mytemp.joinpath(basename).exists() @@ -238,7 +238,7 @@ def test_make(self, tmp_path): assert d.name.startswith(self.PREFIX) assert d.name.endswith(str(i)) - symlink = tmp_path.joinpath(self.PREFIX + "current") + symlink = tmp_path.joinpath(f"{self.PREFIX}current") if symlink.exists(): # unix assert symlink.is_symlink() @@ -302,8 +302,8 @@ def test_cleanup_locked(self, tmp_path): ) def test_cleanup_ignores_symlink(self, tmp_path): - the_symlink = tmp_path / (self.PREFIX + "current") - attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5")) + the_symlink = tmp_path / f"{self.PREFIX}current" + attempt_symlink_to(the_symlink, tmp_path / f"{self.PREFIX}5") self._do_cleanup(tmp_path) def test_removal_accepts_lock(self, tmp_path): diff --git a/testing/test_unittest.py b/testing/test_unittest.py index d917d331a03..f64a131a88f 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -452,11 +452,11 @@ def test_func1(self): class TestTrialUnittest: - def setup_class(cls): - cls.ut = pytest.importorskip("twisted.trial.unittest") + def setup_class(self): + self.ut = pytest.importorskip("twisted.trial.unittest") # on windows trial uses a socket for a reactor and apparently doesn't close it properly # https://twistedmatrix.com/trac/ticket/9227 - cls.ignore_unclosed_socket_warning = ("-W", "always") + self.ignore_unclosed_socket_warning = ("-W", "always") def test_trial_testcase_runtest_not_collected(self, pytester: Pytester) -> None: pytester.makepyfile( @@ -1274,7 +1274,7 @@ def test_1(self): ) result = pytester.runpytest_inprocess("--pdb") result.stdout.fnmatch_lines("* 1 skipped in *") - assert tracked == [] + assert not tracked @pytest.mark.parametrize("mark", ["@unittest.skip", "@pytest.mark.skip"]) @@ -1311,7 +1311,7 @@ def test_1(self): ) result = pytester.runpytest_inprocess("--pdb") result.stdout.fnmatch_lines("* 1 skipped in *") - assert tracked == [] + assert not tracked def test_async_support(pytester: Pytester) -> None: diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 7b716bb4546..38a0a50780b 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -45,7 +45,7 @@ def test_normal_flow(pytester: Pytester, pyfile_with_warnings) -> None: result = pytester.runpytest(pyfile_with_warnings) result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", "test_normal_flow.py::test_func", "*normal_flow_module.py:3: UserWarning: user warning", '* warnings.warn(UserWarning("user warning"))', @@ -76,7 +76,7 @@ def test_func(fix): result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", "*test_setup_teardown_warnings.py:6: UserWarning: warning during setup", '*warnings.warn(UserWarning("warning during setup"))', "*test_setup_teardown_warnings.py:8: UserWarning: warning during teardown", @@ -144,7 +144,7 @@ def test_func(fix): result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", "*test_unicode.py:7: UserWarning: \u6d4b\u8bd5*", "* 1 passed, 1 warning*", ] @@ -316,7 +316,7 @@ def test_foo(): result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", " *collection_warnings.py:3: UserWarning: collection warning", ' warnings.warn(UserWarning("collection warning"))', "* 1 passed, 1 warning*", @@ -375,7 +375,7 @@ def test_bar(): else: result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", "*test_hide_pytest_internal_warnings.py:4: PytestWarning: some internal warning", "* 1 passed, 1 warning *", ] @@ -464,7 +464,7 @@ def test_shown_by_default(self, pytester: Pytester, customize_filters) -> None: result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", "*test_shown_by_default.py:3: DeprecationWarning: collection", "*test_shown_by_default.py:7: PendingDeprecationWarning: test run", "* 1 passed, 2 warnings*", @@ -495,7 +495,7 @@ def test_hidden_by_mark(self, pytester: Pytester) -> None: result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", "*test_hidden_by_mark.py:3: DeprecationWarning: collection", "* 1 passed, 1 warning*", ] @@ -558,7 +558,7 @@ def test(): class TestAssertionWarnings: @staticmethod def assert_result_warns(result, msg) -> None: - result.stdout.fnmatch_lines(["*PytestAssertRewriteWarning: %s*" % msg]) + result.stdout.fnmatch_lines([f"*PytestAssertRewriteWarning: {msg}*"]) def test_tuple_warning(self, pytester: Pytester) -> None: pytester.makepyfile( @@ -588,7 +588,7 @@ def test_group_warnings_by_message(pytester: Pytester) -> None: result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", "test_group_warnings_by_message.py::test_foo[[]0[]]", "test_group_warnings_by_message.py::test_foo[[]1[]]", "test_group_warnings_by_message.py::test_foo[[]2[]]", @@ -620,7 +620,7 @@ def test_group_warnings_by_message_summary(pytester: Pytester) -> None: result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", "test_1.py: 21 warnings", "test_2.py: 1 warning", " */test_1.py:7: UserWarning: foo",