Skip to content

Commit 83bd4d6

Browse files
committed
Mark some public and to-be-public classes as @final
This indicates at least for people using type checkers that these classes are not designed for inheritance and we make no stability guarantees regarding inheritance of them. Currently this doesn't show up in the docs. Sphinx does actually support `@final`, however it only works when imported directly from `typing`, while we import from `_pytest.compat`. In the future there might also be a `@sealed` decorator which would cover some more cases.
1 parent cdfdb3a commit 83bd4d6

22 files changed

+78
-1
lines changed

src/_pytest/_code/code.py

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from _pytest._io.saferepr import safeformat
3939
from _pytest._io.saferepr import saferepr
4040
from _pytest.compat import ATTRS_EQ_FIELD
41+
from _pytest.compat import final
4142
from _pytest.compat import get_real_func
4243
from _pytest.compat import overload
4344
from _pytest.compat import TYPE_CHECKING
@@ -414,6 +415,7 @@ def recursionindex(self) -> Optional[int]:
414415
_E = TypeVar("_E", bound=BaseException, covariant=True)
415416

416417

418+
@final
417419
@attr.s(repr=False)
418420
class ExceptionInfo(Generic[_E]):
419421
"""Wraps sys.exc_info() objects and offers help for navigating the traceback."""

src/_pytest/_io/terminalwriter.py

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import TextIO
88

99
from .wcwidth import wcswidth
10+
from _pytest.compat import final
1011

1112

1213
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
@@ -36,6 +37,7 @@ def should_do_markup(file: TextIO) -> bool:
3637
)
3738

3839

40+
@final
3941
class TerminalWriter:
4042
_esctable = dict(
4143
black=30,

src/_pytest/cacheprovider.py

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from .reports import CollectReport
2222
from _pytest import nodes
2323
from _pytest._io import TerminalWriter
24+
from _pytest.compat import final
2425
from _pytest.compat import order_preserving_dict
2526
from _pytest.config import Config
2627
from _pytest.config import ExitCode
@@ -50,6 +51,7 @@
5051
"""
5152

5253

54+
@final
5355
@attr.s
5456
class Cache:
5557
_cachedir = attr.ib(type=Path, repr=False)

src/_pytest/capture.py

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from typing import Union
1818

1919
import pytest
20+
from _pytest.compat import final
2021
from _pytest.compat import TYPE_CHECKING
2122
from _pytest.config import Config
2223
from _pytest.config.argparsing import Parser
@@ -498,6 +499,7 @@ def writeorg(self, data):
498499
# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can
499500
# make it a namedtuple again.
500501
# [0]: https://github.com/python/mypy/issues/685
502+
@final
501503
@functools.total_ordering
502504
class CaptureResult(Generic[AnyStr]):
503505
"""The result of :method:`CaptureFixture.readouterr`."""

src/_pytest/compat.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
import attr
2121

22-
from _pytest._io.saferepr import saferepr
2322
from _pytest.outcomes import fail
2423
from _pytest.outcomes import TEST_OUTCOME
2524

@@ -297,6 +296,8 @@ def get_real_func(obj):
297296
break
298297
obj = new_obj
299298
else:
299+
from _pytest._io.saferepr import saferepr
300+
300301
raise ValueError(
301302
("could not find real function of {start}\nstopped at {current}").format(
302303
start=saferepr(start_obj), current=saferepr(obj)
@@ -357,6 +358,19 @@ def overload(f): # noqa: F811
357358
return f
358359

359360

361+
if TYPE_CHECKING:
362+
if sys.version_info >= (3, 8):
363+
from typing import final as final
364+
else:
365+
from typing_extensions import final as final
366+
elif sys.version_info >= (3, 8):
367+
from typing import final as final
368+
else:
369+
370+
def final(f): # noqa: F811
371+
return f
372+
373+
360374
if getattr(attr, "__version_info__", ()) >= (19, 2):
361375
ATTRS_EQ_FIELD = "eq"
362376
else:

src/_pytest/config/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from _pytest._code import ExceptionInfo
4444
from _pytest._code import filter_traceback
4545
from _pytest._io import TerminalWriter
46+
from _pytest.compat import final
4647
from _pytest.compat import importlib_metadata
4748
from _pytest.compat import TYPE_CHECKING
4849
from _pytest.outcomes import fail
@@ -76,6 +77,7 @@
7677
hookspec = HookspecMarker("pytest")
7778

7879

80+
@final
7981
class ExitCode(enum.IntEnum):
8082
"""Encodes the valid exit codes by pytest.
8183
@@ -322,6 +324,7 @@ def _prepareconfig(
322324
raise
323325

324326

327+
@final
325328
class PytestPluginManager(PluginManager):
326329
"""A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with
327330
additional pytest-specific functionality:
@@ -815,6 +818,7 @@ def _args_converter(args: Iterable[str]) -> Tuple[str, ...]:
815818
return tuple(args)
816819

817820

821+
@final
818822
class Config:
819823
"""Access to configuration values, pluginmanager and plugin hooks.
820824
@@ -825,6 +829,7 @@ class Config:
825829
invocation.
826830
"""
827831

832+
@final
828833
@attr.s(frozen=True)
829834
class InvocationParams:
830835
"""Holds parameters passed during :func:`pytest.main`.

src/_pytest/config/argparsing.py

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import py
1717

1818
import _pytest._io
19+
from _pytest.compat import final
1920
from _pytest.compat import TYPE_CHECKING
2021
from _pytest.config.exceptions import UsageError
2122

@@ -26,6 +27,7 @@
2627
FILE_OR_DIR = "file_or_dir"
2728

2829

30+
@final
2931
class Parser:
3032
"""Parser for command line arguments and ini-file values.
3133

src/_pytest/config/exceptions.py

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from _pytest.compat import final
2+
3+
4+
@final
15
class UsageError(Exception):
26
"""Error in pytest usage or invocation."""
37

src/_pytest/fixtures.py

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from _pytest._io import TerminalWriter
3333
from _pytest.compat import _format_args
3434
from _pytest.compat import _PytestWrapper
35+
from _pytest.compat import final
3536
from _pytest.compat import get_real_func
3637
from _pytest.compat import get_real_method
3738
from _pytest.compat import getfuncargnames
@@ -730,6 +731,7 @@ def __repr__(self) -> str:
730731
return "<FixtureRequest for %r>" % (self.node)
731732

732733

734+
@final
733735
class SubRequest(FixtureRequest):
734736
"""A sub request for handling getting a fixture from a test function/fixture."""
735737

@@ -796,6 +798,7 @@ def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int:
796798
)
797799

798800

801+
@final
799802
class FixtureLookupError(LookupError):
800803
"""Could not return a requested fixture (missing or invalid)."""
801804

@@ -952,6 +955,7 @@ def _eval_scope_callable(
952955
return result
953956

954957

958+
@final
955959
class FixtureDef(Generic[_FixtureValue]):
956960
"""A container for a factory definition."""
957961

@@ -1161,6 +1165,7 @@ def result(*args, **kwargs):
11611165
return result
11621166

11631167

1168+
@final
11641169
@attr.s(frozen=True)
11651170
class FixtureFunctionMarker:
11661171
scope = attr.ib(type="Union[_Scope, Callable[[str, Config], _Scope]]")

src/_pytest/logging.py

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from _pytest import nodes
2020
from _pytest._io import TerminalWriter
2121
from _pytest.capture import CaptureManager
22+
from _pytest.compat import final
2223
from _pytest.compat import nullcontext
2324
from _pytest.config import _strtobool
2425
from _pytest.config import Config
@@ -339,6 +340,7 @@ def handleError(self, record: logging.LogRecord) -> None:
339340
raise
340341

341342

343+
@final
342344
class LogCaptureFixture:
343345
"""Provides access and control of log capturing."""
344346

src/_pytest/main.py

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import _pytest._code
2323
from _pytest import nodes
24+
from _pytest.compat import final
2425
from _pytest.compat import overload
2526
from _pytest.compat import TYPE_CHECKING
2627
from _pytest.config import Config
@@ -435,6 +436,7 @@ def __missing__(self, path: Path) -> str:
435436
return r
436437

437438

439+
@final
438440
class Session(nodes.FSCollector):
439441
Interrupted = Interrupted
440442
Failed = Failed

src/_pytest/mark/structures.py

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from .._code import getfslineno
2222
from ..compat import ascii_escaped
23+
from ..compat import final
2324
from ..compat import NOTSET
2425
from ..compat import NotSetType
2526
from ..compat import overload
@@ -199,6 +200,7 @@ def _for_parametrize(
199200
return argnames, parameters
200201

201202

203+
@final
202204
@attr.s(frozen=True)
203205
class Mark:
204206
#: Name of the mark.
@@ -452,6 +454,7 @@ def __call__( # type: ignore[override]
452454
...
453455

454456

457+
@final
455458
class MarkGenerator:
456459
"""Factory for :class:`MarkDecorator` objects - exposed as
457460
a ``pytest.mark`` singleton instance.
@@ -525,6 +528,7 @@ def __getattr__(self, name: str) -> MarkDecorator:
525528

526529

527530
# TODO(py36): inherit from typing.MutableMapping[str, Any].
531+
@final
528532
class NodeKeywords(collections.abc.MutableMapping): # type: ignore[type-arg]
529533
def __init__(self, node: "Node") -> None:
530534
self.node = node

src/_pytest/monkeypatch.py

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from typing import Union
1515

1616
import pytest
17+
from _pytest.compat import final
1718
from _pytest.compat import overload
1819
from _pytest.fixtures import fixture
1920
from _pytest.pathlib import Path
@@ -110,6 +111,7 @@ def __repr__(self) -> str:
110111
notset = Notset()
111112

112113

114+
@final
113115
class MonkeyPatch:
114116
"""Object returned by the ``monkeypatch`` fixture keeping a record of
115117
setattr/item/env/syspath changes."""

src/_pytest/pytester.py

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from _pytest import timing
2929
from _pytest._code import Source
3030
from _pytest.capture import _get_multicapture
31+
from _pytest.compat import final
3132
from _pytest.compat import overload
3233
from _pytest.compat import TYPE_CHECKING
3334
from _pytest.config import _PluggyPlugin
@@ -597,6 +598,7 @@ def restore(self) -> None:
597598
sys.path[:], sys.meta_path[:] = self.__saved
598599

599600

601+
@final
600602
class Testdir:
601603
"""Temporary test directory with tools to test/run pytest itself.
602604

src/_pytest/python.py

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from _pytest._io import TerminalWriter
3838
from _pytest._io.saferepr import saferepr
3939
from _pytest.compat import ascii_escaped
40+
from _pytest.compat import final
4041
from _pytest.compat import get_default_arg_names
4142
from _pytest.compat import get_real_func
4243
from _pytest.compat import getimfunc
@@ -864,6 +865,7 @@ def hasnew(obj: object) -> bool:
864865
return False
865866

866867

868+
@final
867869
class CallSpec2:
868870
def __init__(self, metafunc: "Metafunc") -> None:
869871
self.metafunc = metafunc
@@ -924,6 +926,7 @@ def setmulti2(
924926
self.marks.extend(normalize_mark_list(marks))
925927

926928

929+
@final
927930
class Metafunc:
928931
"""Objects passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook.
929932

src/_pytest/python_api.py

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from typing import Union
1818

1919
import _pytest._code
20+
from _pytest.compat import final
2021
from _pytest.compat import overload
2122
from _pytest.compat import STRING_TYPES
2223
from _pytest.compat import TYPE_CHECKING
@@ -699,6 +700,7 @@ def raises( # noqa: F811
699700
raises.Exception = fail.Exception # type: ignore
700701

701702

703+
@final
702704
class RaisesContext(Generic[_E]):
703705
def __init__(
704706
self,

src/_pytest/recwarn.py

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from typing import TypeVar
1414
from typing import Union
1515

16+
from _pytest.compat import final
1617
from _pytest.compat import overload
1718
from _pytest.compat import TYPE_CHECKING
1819
from _pytest.fixtures import fixture
@@ -228,6 +229,7 @@ def __exit__(
228229
self._entered = False
229230

230231

232+
@final
231233
class WarningsChecker(WarningsRecorder):
232234
def __init__(
233235
self,

src/_pytest/reports.py

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from _pytest._code.code import ReprTraceback
2727
from _pytest._code.code import TerminalRepr
2828
from _pytest._io import TerminalWriter
29+
from _pytest.compat import final
2930
from _pytest.compat import TYPE_CHECKING
3031
from _pytest.config import Config
3132
from _pytest.nodes import Collector
@@ -225,6 +226,7 @@ def _report_unserialization_failure(
225226
raise RuntimeError(stream.getvalue())
226227

227228

229+
@final
228230
class TestReport(BaseReport):
229231
"""Basic test report object (also used for setup and teardown calls if
230232
they fail)."""
@@ -333,6 +335,7 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
333335
)
334336

335337

338+
@final
336339
class CollectReport(BaseReport):
337340
"""Collection report object."""
338341

src/_pytest/runner.py

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from _pytest._code.code import ExceptionChainRepr
2323
from _pytest._code.code import ExceptionInfo
2424
from _pytest._code.code import TerminalRepr
25+
from _pytest.compat import final
2526
from _pytest.compat import TYPE_CHECKING
2627
from _pytest.config.argparsing import Parser
2728
from _pytest.nodes import Collector
@@ -259,6 +260,7 @@ def call_runtest_hook(
259260
TResult = TypeVar("TResult", covariant=True)
260261

261262

263+
@final
262264
@attr.s(repr=False)
263265
class CallInfo(Generic[TResult]):
264266
"""Result/Exception info a function invocation.

0 commit comments

Comments
 (0)