diff --git a/pyproject.toml b/pyproject.toml index ebfbe1a03..e3d64805d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,15 +128,24 @@ ignore = "W009" src = ["src"] line-length = 88 select = [ + "B", # bugbear "D", # pydocstyle "E", # pycodestyle "F", # pyflakes "I", # isort + "PYI", # flake8-pyi "UP", # pyupgrade "RUF", # ruff "W", # pycodestyle ] ignore = [ + # bugbear ignore + "B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable. + "B007", # Loop control variable `i` not used within loop body + "B009", # Do not call `getattr` with a constant attribute value + "B010", # [*] Do not call `setattr` with a constant attribute value. + "B011", # Do not `assert False` (`python -O` removes these calls) + "B028", # No explicit `stacklevel` keyword argument found # pycodestyle ignore # pytest can do weird low-level things, and we usually know # what we're doing when we use type(..) is ... @@ -180,4 +189,6 @@ known-local-folder = ["pytest", "_pytest"] lines-after-imports = 2 [tool.ruff.lint.per-file-ignores] +"src/_pytest/_py/**/*.py" = ["B", "PYI"] "src/_pytest/_version.py" = ["I001"] +"testing/python/approx.py" = ["B015"] diff --git a/scripts/prepare-release-pr.py b/scripts/prepare-release-pr.py index d2216b6fc..8a9f0aa0f 100644 --- a/scripts/prepare-release-pr.py +++ b/scripts/prepare-release-pr.py @@ -79,7 +79,7 @@ def prepare_release_pr( ) except InvalidFeatureRelease as e: print(f"{Fore.RED}{e}") - raise SystemExit(1) + raise SystemExit(1) from None print(f"Version: {Fore.CYAN}{version}") diff --git a/scripts/update-plugin-list.py b/scripts/update-plugin-list.py index 7c5ac9277..2c57414c0 100644 --- a/scripts/update-plugin-list.py +++ b/scripts/update-plugin-list.py @@ -208,7 +208,7 @@ def main() -> None: f.write(f"This list contains {len(plugins)} plugins.\n\n") f.write(".. only:: not latex\n\n") - wcwidth # reference library that must exist for tabulate to work + _ = wcwidth # reference library that must exist for tabulate to work plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst") f.write(indent(plugin_table, " ")) f.write("\n\n") diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 16449b780..badbb7e4a 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -232,17 +232,17 @@ class TerminalWriter: # which may lead to the previous color being propagated to the # start of the expression, so reset first. return "\x1b[0m" + highlighted - except pygments.util.ClassNotFound: + except pygments.util.ClassNotFound as e: raise UsageError( "PYTEST_THEME environment variable had an invalid value: '{}'. " "Only valid pygment styles are allowed.".format( os.getenv("PYTEST_THEME") ) - ) - except pygments.util.OptionError: + ) from e + except pygments.util.OptionError as e: 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") ) - ) + ) from e diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index b9e095028..dce431c3d 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -598,7 +598,8 @@ if sys.version_info >= (3, 11) or TYPE_CHECKING: else: class CaptureResult( - collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr] + collections.namedtuple("CaptureResult", ["out", "err"]), # noqa: PYI024 + Generic[AnyStr], ): """The result of :method:`caplog.readouterr() `.""" diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 14717e941..400b38642 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -15,11 +15,6 @@ from typing import Any from typing import Callable from typing import Final from typing import NoReturn -from typing import TypeVar - - -_T = TypeVar("_T") -_S = TypeVar("_S") # fmt: off diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 0d48ef489..cada2aa09 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1848,13 +1848,13 @@ def parse_warning_filter( try: action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined] except warnings._OptionError as e: - raise UsageError(error_template.format(error=str(e))) + raise UsageError(error_template.format(error=str(e))) from None try: category: Type[Warning] = _resolve_warning_category(category_) except Exception: exc_info = ExceptionInfo.from_current() exception_text = exc_info.getrepr(style="native") - raise UsageError(error_template.format(error=exception_text)) + raise UsageError(error_template.format(error=exception_text)) from None if message and escape: message = re.escape(message) if module and escape: @@ -1867,7 +1867,7 @@ def parse_warning_filter( except ValueError as e: raise UsageError( error_template.format(error=f"invalid lineno {lineno_!r}: {e}") - ) + ) from None else: lineno = 0 return action, message, category, module, lineno diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 29a53ad5c..6598bdbc5 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -209,8 +209,8 @@ class TestCaseFunction(Function): ) # Invoke the attributes to trigger storing the traceback # trial causes some issue there. - excinfo.value - excinfo.traceback + _ = excinfo.value + _ = excinfo.traceback except TypeError: try: try: @@ -361,14 +361,21 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: # Twisted trial support. +classImplements_has_run = False @hookimpl(wrapper=True) def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules: ut: Any = sys.modules["twisted.python.failure"] + global classImplements_has_run Failure__init__ = ut.Failure.__init__ - check_testcase_implements_trial_reporter() + if not classImplements_has_run: + from twisted.trial.itrial import IReporter + from zope.interface import classImplements + + classImplements(TestCaseFunction, IReporter) + classImplements_has_run = True def excstore( self, exc_value=None, exc_type=None, exc_tb=None, captureVars=None @@ -396,16 +403,6 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: return res -def check_testcase_implements_trial_reporter(done: List[int] = []) -> None: - if done: - return - from twisted.trial.itrial import IReporter - from zope.interface import classImplements - - classImplements(TestCaseFunction, IReporter) - done.append(1) - - def _is_skipped(obj) -> bool: """Return True if the given object has been marked with @unittest.skip.""" return bool(getattr(obj, "__unittest_skip__", False)) diff --git a/testing/_py/test_local.py b/testing/_py/test_local.py index 11519c7c1..0c8575c4e 100644 --- a/testing/_py/test_local.py +++ b/testing/_py/test_local.py @@ -1241,9 +1241,9 @@ class TestWINLocalPath: def test_owner_group_not_implemented(self, path1): with pytest.raises(NotImplementedError): - path1.stat().owner + _ = path1.stat().owner with pytest.raises(NotImplementedError): - path1.stat().group + _ = path1.stat().group def test_chmod_simple_int(self, path1): mode = path1.stat().mode diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index a0ee28d48..cce23bf87 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -387,7 +387,7 @@ def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None: excinfo = pytest.raises(ValueError, template.render, h=h) for item in excinfo.traceback: print(item) # XXX: for some reason jinja.Template.render is printed in full - item.source # shouldn't fail + _ = item.source # shouldn't fail if isinstance(item.path, Path) and item.path.name == "test.txt": assert str(item.source) == "{{ h()}}:" @@ -418,7 +418,7 @@ def test_codepath_Queue_example() -> None: def test_match_succeeds(): with pytest.raises(ZeroDivisionError) as excinfo: - 0 // 0 + _ = 0 // 0 excinfo.match(r".*zero.*") @@ -584,7 +584,7 @@ class TestFormattedExcinfo: try: def f(): - 1 / 0 + _ = 1 / 0 f() @@ -601,7 +601,7 @@ class TestFormattedExcinfo: print(line) assert lines == [ " def f():", - "> 1 / 0", + "> _ = 1 / 0", "E ZeroDivisionError: division by zero", ] @@ -638,7 +638,7 @@ raise ValueError() pr = FormattedExcinfo() try: - 1 / 0 + _ = 1 / 0 except ZeroDivisionError: excinfo = ExceptionInfo.from_current() @@ -1582,7 +1582,7 @@ def test_no_recursion_index_on_recursion_error(): return getattr(self, "_" + attr) with pytest.raises(RuntimeError) as excinfo: - RecursionDepthError().trigger + _ = RecursionDepthError().trigger assert "maximum recursion" in str(excinfo.getrepr()) diff --git a/testing/python/raises.py b/testing/python/raises.py index ef6607d96..929865e31 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -280,7 +280,7 @@ class TestRaises: def test_raises_context_manager_with_kwargs(self): with pytest.raises(TypeError) as excinfo: - with pytest.raises(Exception, foo="bar"): # type: ignore[call-overload] + with pytest.raises(OSError, foo="bar"): # type: ignore[call-overload] pass assert "Unexpected keyword arguments" in str(excinfo.value) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 2fa6fbe37..2d92128fb 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1,10 +1,10 @@ # mypy: allow-untyped-defs -import collections import sys import textwrap from typing import Any from typing import List from typing import MutableSequence +from typing import NamedTuple from typing import Optional import attr @@ -1179,7 +1179,9 @@ class TestAssert_reprcompare_attrsclass: class TestAssert_reprcompare_namedtuple: def test_namedtuple(self) -> None: - NT = collections.namedtuple("NT", ["a", "b"]) + class NT(NamedTuple): + a: Any + b: Any left = NT(1, "b") right = NT(1, "c") @@ -1200,8 +1202,13 @@ class TestAssert_reprcompare_namedtuple: ] def test_comparing_two_different_namedtuple(self) -> None: - NT1 = collections.namedtuple("NT1", ["a", "b"]) - NT2 = collections.namedtuple("NT2", ["a", "b"]) + class NT1(NamedTuple): + a: Any + b: Any + + class NT2(NamedTuple): + a: Any + b: Any left = NT1(1, "b") right = NT2(2, "b") diff --git a/testing/test_compat.py b/testing/test_compat.py index 4ea905354..c898af7c5 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -169,17 +169,17 @@ class ErrorsHelper: def test_helper_failures() -> None: helper = ErrorsHelper() - with pytest.raises(Exception): - helper.raise_exception + with pytest.raises(Exception): # noqa: B017 + _ = helper.raise_exception with pytest.raises(OutcomeException): - helper.raise_fail_outcome + _ = helper.raise_fail_outcome def test_safe_getattr() -> None: helper = ErrorsHelper() assert safe_getattr(helper, "raise_exception", "default") == "default" assert safe_getattr(helper, "raise_fail_outcome", "default") == "default" - with pytest.raises(BaseException): + with pytest.raises(BaseException): # noqa: B017 assert safe_getattr(helper, "raise_baseexception", "default") diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py index 6b933a6d9..850f14c58 100644 --- a/testing/test_legacypath.py +++ b/testing/test_legacypath.py @@ -108,7 +108,7 @@ class TestFixtureRequestSessionScoped: AttributeError, match="path not available in session-scoped context", ): - session_request.fspath + _ = session_request.fspath @pytest.mark.parametrize("config_type", ["ini", "pyproject"]) diff --git a/testing/test_mark.py b/testing/test_mark.py index 4604baafd..6e183a178 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -42,7 +42,7 @@ class TestMark: def test_pytest_mark_name_starts_with_underscore(self) -> None: mark = MarkGenerator(_ispytest=True) with pytest.raises(AttributeError): - mark._some_name + _ = mark._some_name def test_marked_class_run_twice(pytester: Pytester) -> None: diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index e2b2eb8a2..5045c781e 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -228,7 +228,7 @@ class TestDeprecatedCall: for warning in other_warnings: def f(): - warnings.warn(warning("hi")) + warnings.warn(warning("hi")) # noqa: B023 with pytest.warns(warning): with pytest.raises(pytest.fail.Exception): diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 22f041ced..bc457c398 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1,6 +1,5 @@ # mypy: allow-untyped-defs """Terminal reporting of the full testing process.""" -import collections from io import StringIO import os from pathlib import Path @@ -10,6 +9,7 @@ from types import SimpleNamespace from typing import cast from typing import Dict from typing import List +from typing import NamedTuple from typing import Tuple import pluggy @@ -34,7 +34,9 @@ from _pytest.terminal import TerminalReporter import pytest -DistInfo = collections.namedtuple("DistInfo", ["project_name", "version"]) +class DistInfo(NamedTuple): + project_name: str + version: int TRANS_FNMATCH = str.maketrans({"[": "[[]", "]": "[]]"}) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 5a8a98015..5b2f27139 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -18,8 +18,7 @@ WARNINGS_SUMMARY_HEADER = "warnings summary" def pyfile_with_warnings(pytester: Pytester, request: FixtureRequest) -> str: """Create a test file which calls a function in a module which generates warnings.""" pytester.syspathinsert() - test_name = request.function.__name__ - module_name = test_name.lstrip("test_") + "_module" + module_name = request.function.__name__[len("test_") :] + "_module" test_file = pytester.makepyfile( f""" import {module_name}