Skip to content

Commit 5cfd7c0

Browse files
authored
Merge pull request #7780 from bluetech/final
Mark some public and to-be-public classes as `@final`
2 parents 050c2df + a99ca87 commit 5cfd7c0

23 files changed

+81
-1
lines changed

changelog/7780.improvement.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Public classes which are not designed to be inherited from are now marked `@final <https://docs.python.org/3/library/typing.html#typing.final>`_.
2+
Code which inherits from these classes will trigger a type-checking (e.g. mypy) error, but will still work in runtime.
3+
Currently the ``final`` designation does not appear in the API Reference but hopefully will in the future.

src/_pytest/_code/code.py

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 15 additions & 1 deletion
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

0 commit comments

Comments
 (0)