Skip to content

Commit a99ca87

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 a99ca87

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]]")

0 commit comments

Comments
 (0)