From 7eef4619d591e0720d20a770dd0f837a389ef415 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 4 Feb 2024 11:24:47 +0100 Subject: [PATCH 1/9] [flake8-bugbear] Add checks from flake8 bugbear --- pyproject.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ebfbe1a03..0876b1f33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,6 +128,7 @@ ignore = "W009" src = ["src"] line-length = 88 select = [ + "B", # bugbear "D", # pydocstyle "E", # pycodestyle "F", # pyflakes @@ -137,6 +138,20 @@ select = [ "W", # pycodestyle ] ignore = [ + # bugbear ignore + "B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable. + "B005", # Using `.strip()` with multi-character strings is misleading + "B006", # Do not use mutable data structures for argument defaults + "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) + "B015", # Pointless comparison. Did you mean to assign a value? + "B017", # `pytest.raises(Exception)` should be considered evil + "B018", # Found useless expression. + "B023", # Function definition does not bind loop variable `warning` + "B028", # No explicit `stacklevel` keyword argument found + "B904", # Within an `except` clause, raise exceptions with `raise ... from err` # pycodestyle ignore # pytest can do weird low-level things, and we usually know # what we're doing when we use type(..) is ... @@ -181,3 +196,4 @@ lines-after-imports = 2 [tool.ruff.lint.per-file-ignores] "src/_pytest/_version.py" = ["I001"] +"src/_pytest/_py/**/*.py" = ["B", "PYI"] From fcb818b73ca7bd0d005cec8f9b5aba47c4876de3 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 4 Feb 2024 11:13:47 +0100 Subject: [PATCH 2/9] [flake8-bugbear] Re-raise all exceptions with proper exception chaining --- pyproject.toml | 1 - scripts/prepare-release-pr.py | 2 +- src/_pytest/_io/terminalwriter.py | 8 ++++---- src/_pytest/config/__init__.py | 6 +++--- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0876b1f33..bda2091a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -151,7 +151,6 @@ ignore = [ "B018", # Found useless expression. "B023", # Function definition does not bind loop variable `warning` "B028", # No explicit `stacklevel` keyword argument found - "B904", # Within an `except` clause, raise exceptions with `raise ... from err` # pycodestyle ignore # pytest can do weird low-level things, and we usually know # what we're doing when we use type(..) is ... 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/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/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 From 52fba25ff94f01e510ff5e7f52da5bb74a32669e Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Fri, 2 Feb 2024 21:13:43 +0100 Subject: [PATCH 3/9] [flake8-bugbear] Fix all the useless expressions that are justified --- pyproject.toml | 1 - scripts/update-plugin-list.py | 2 +- src/_pytest/unittest.py | 4 ++-- testing/_py/test_local.py | 4 ++-- testing/code/test_excinfo.py | 12 ++++++------ testing/test_compat.py | 4 ++-- testing/test_legacypath.py | 2 +- testing/test_mark.py | 2 +- 8 files changed, 15 insertions(+), 16 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bda2091a3..7adb20467 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -148,7 +148,6 @@ ignore = [ "B011", # Do not `assert False` (`python -O` removes these calls) "B015", # Pointless comparison. Did you mean to assign a value? "B017", # `pytest.raises(Exception)` should be considered evil - "B018", # Found useless expression. "B023", # Function definition does not bind loop variable `warning` "B028", # No explicit `stacklevel` keyword argument found # pycodestyle ignore 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/unittest.py b/src/_pytest/unittest.py index 29a53ad5c..eccd7eca6 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: 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/test_compat.py b/testing/test_compat.py index 4ea905354..9e66e9eca 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -170,9 +170,9 @@ class ErrorsHelper: def test_helper_failures() -> None: helper = ErrorsHelper() with pytest.raises(Exception): - helper.raise_exception + _ = helper.raise_exception with pytest.raises(OutcomeException): - helper.raise_fail_outcome + _ = helper.raise_fail_outcome def test_safe_getattr() -> None: 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: From e7bab63537ab66f2f750e72627c8fef3ccfddc9f Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Fri, 2 Feb 2024 21:15:22 +0100 Subject: [PATCH 4/9] [flake8-bugbear] noqa all the useless comparison that are justified --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7adb20467..062970ca7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,7 +146,6 @@ ignore = [ "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) - "B015", # Pointless comparison. Did you mean to assign a value? "B017", # `pytest.raises(Exception)` should be considered evil "B023", # Function definition does not bind loop variable `warning` "B028", # No explicit `stacklevel` keyword argument found @@ -193,5 +192,6 @@ known-local-folder = ["pytest", "_pytest"] lines-after-imports = 2 [tool.ruff.lint.per-file-ignores] -"src/_pytest/_version.py" = ["I001"] "src/_pytest/_py/**/*.py" = ["B", "PYI"] +"src/_pytest/_version.py" = ["I001"] +"testing/python/approx.py" = ["B015"] From b62d4b352748729c9e36f450226d191335177730 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Fri, 2 Feb 2024 21:21:39 +0100 Subject: [PATCH 5/9] [flake8-bugbear] Remove misleading multiple characters in lstrip See https://pylint.readthedocs.io/en/stable/user_guide/messages/error/bad-str-strip-call.html --- pyproject.toml | 1 - testing/test_warnings.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 062970ca7..89aab6a07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,7 +140,6 @@ select = [ ignore = [ # bugbear ignore "B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable. - "B005", # Using `.strip()` with multi-character strings is misleading "B006", # Do not use mutable data structures for argument defaults "B007", # Loop control variable `i` not used within loop body "B009", # Do not call `getattr` with a constant attribute value 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} From 41ff3584d78091bf2753bb084b1f5ec8a97c3f51 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 4 Feb 2024 11:39:27 +0100 Subject: [PATCH 6/9] [flake8-bugbear] Fixes a B017 we can actually fix and noqa the two others --- pyproject.toml | 1 - testing/python/raises.py | 2 +- testing/test_compat.py | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 89aab6a07..cf6e5adec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,7 +145,6 @@ ignore = [ "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) - "B017", # `pytest.raises(Exception)` should be considered evil "B023", # Function definition does not bind loop variable `warning` "B028", # No explicit `stacklevel` keyword argument found # pycodestyle ignore 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_compat.py b/testing/test_compat.py index 9e66e9eca..c898af7c5 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -169,7 +169,7 @@ class ErrorsHelper: def test_helper_failures() -> None: helper = ErrorsHelper() - with pytest.raises(Exception): + with pytest.raises(Exception): # noqa: B017 _ = helper.raise_exception with pytest.raises(OutcomeException): _ = helper.raise_fail_outcome @@ -179,7 +179,7 @@ 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") From 4eb246d4e15de8e6d089d35c30d1102fb4f46032 Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Fri, 2 Feb 2024 21:26:49 +0100 Subject: [PATCH 7/9] [flake8-bugbear] noqa B023 not bound by design --- pyproject.toml | 1 - testing/test_recwarn.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cf6e5adec..8605e5bae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,7 +145,6 @@ ignore = [ "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) - "B023", # Function definition does not bind loop variable `warning` "B028", # No explicit `stacklevel` keyword argument found # pycodestyle ignore # pytest can do weird low-level things, and we usually know 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): From e193a263c70549815377158dd0ddc9e325f6dfcf Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Fri, 2 Feb 2024 21:36:20 +0100 Subject: [PATCH 8/9] [flake8-pyi] Add checks for flake8-pyi and fix existing --- pyproject.toml | 1 + src/_pytest/capture.py | 3 ++- src/_pytest/compat.py | 5 ----- testing/test_assertion.py | 15 +++++++++++---- testing/test_terminal.py | 6 ++++-- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8605e5bae..634b08cbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -133,6 +133,7 @@ select = [ "E", # pycodestyle "F", # pyflakes "I", # isort + "PYI", # flake8-pyi "UP", # pyupgrade "RUF", # ruff "W", # pycodestyle 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/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_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({"[": "[[]", "]": "[]]"}) From 3101c026b96e15f155aa95e3e3321dbee586643c Mon Sep 17 00:00:00 2001 From: Pierre Sassoulas Date: Sun, 4 Feb 2024 19:39:21 +0100 Subject: [PATCH 9/9] [flake8-bugbear] Remove hidden global state to import only once --- pyproject.toml | 1 - src/_pytest/unittest.py | 19 ++++++++----------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 634b08cbd..e3d64805d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,7 +141,6 @@ select = [ ignore = [ # bugbear ignore "B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable. - "B006", # Do not use mutable data structures for argument defaults "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. diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index eccd7eca6..6598bdbc5 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -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))