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.
This commit is contained in:
Ran Benita 2020-09-21 17:45:24 +03:00
parent cdfdb3a25d
commit a99ca879e7
23 changed files with 81 additions and 1 deletions

View File

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

View File

@ -38,6 +38,7 @@ from _pytest._io import TerminalWriter
from _pytest._io.saferepr import safeformat
from _pytest._io.saferepr import saferepr
from _pytest.compat import ATTRS_EQ_FIELD
from _pytest.compat import final
from _pytest.compat import get_real_func
from _pytest.compat import overload
from _pytest.compat import TYPE_CHECKING
@ -414,6 +415,7 @@ co_equal = compile(
_E = TypeVar("_E", bound=BaseException, covariant=True)
@final
@attr.s(repr=False)
class ExceptionInfo(Generic[_E]):
"""Wraps sys.exc_info() objects and offers help for navigating the traceback."""

View File

@ -7,6 +7,7 @@ from typing import Sequence
from typing import TextIO
from .wcwidth import wcswidth
from _pytest.compat import final
# 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:
)
@final
class TerminalWriter:
_esctable = dict(
black=30,

View File

@ -21,6 +21,7 @@ from .pathlib import rm_rf
from .reports import CollectReport
from _pytest import nodes
from _pytest._io import TerminalWriter
from _pytest.compat import final
from _pytest.compat import order_preserving_dict
from _pytest.config import Config
from _pytest.config import ExitCode
@ -50,6 +51,7 @@ Signature: 8a477f597d28d172789f06886806bc55
"""
@final
@attr.s
class Cache:
_cachedir = attr.ib(type=Path, repr=False)

View File

@ -17,6 +17,7 @@ from typing import Tuple
from typing import Union
import pytest
from _pytest.compat import final
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.config.argparsing import Parser
@ -498,6 +499,7 @@ class FDCapture(FDCaptureBinary):
# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can
# make it a namedtuple again.
# [0]: https://github.com/python/mypy/issues/685
@final
@functools.total_ordering
class CaptureResult(Generic[AnyStr]):
"""The result of :method:`CaptureFixture.readouterr`."""

View File

@ -19,7 +19,6 @@ from typing import Union
import attr
from _pytest._io.saferepr import saferepr
from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME
@ -297,6 +296,8 @@ def get_real_func(obj):
break
obj = new_obj
else:
from _pytest._io.saferepr import saferepr
raise ValueError(
("could not find real function of {start}\nstopped at {current}").format(
start=saferepr(start_obj), current=saferepr(obj)
@ -357,6 +358,19 @@ if sys.version_info < (3, 5, 2):
return f
if TYPE_CHECKING:
if sys.version_info >= (3, 8):
from typing import final as final
else:
from typing_extensions import final as final
elif sys.version_info >= (3, 8):
from typing import final as final
else:
def final(f): # noqa: F811
return f
if getattr(attr, "__version_info__", ()) >= (19, 2):
ATTRS_EQ_FIELD = "eq"
else:

View File

@ -43,6 +43,7 @@ from .findpaths import determine_setup
from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback
from _pytest._io import TerminalWriter
from _pytest.compat import final
from _pytest.compat import importlib_metadata
from _pytest.compat import TYPE_CHECKING
from _pytest.outcomes import fail
@ -76,6 +77,7 @@ hookimpl = HookimplMarker("pytest")
hookspec = HookspecMarker("pytest")
@final
class ExitCode(enum.IntEnum):
"""Encodes the valid exit codes by pytest.
@ -322,6 +324,7 @@ def _prepareconfig(
raise
@final
class PytestPluginManager(PluginManager):
"""A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with
additional pytest-specific functionality:
@ -815,6 +818,7 @@ def _args_converter(args: Iterable[str]) -> Tuple[str, ...]:
return tuple(args)
@final
class Config:
"""Access to configuration values, pluginmanager and plugin hooks.
@ -825,6 +829,7 @@ class Config:
invocation.
"""
@final
@attr.s(frozen=True)
class InvocationParams:
"""Holds parameters passed during :func:`pytest.main`.

View File

@ -16,6 +16,7 @@ from typing import Union
import py
import _pytest._io
from _pytest.compat import final
from _pytest.compat import TYPE_CHECKING
from _pytest.config.exceptions import UsageError
@ -26,6 +27,7 @@ if TYPE_CHECKING:
FILE_OR_DIR = "file_or_dir"
@final
class Parser:
"""Parser for command line arguments and ini-file values.

View File

@ -1,3 +1,7 @@
from _pytest.compat import final
@final
class UsageError(Exception):
"""Error in pytest usage or invocation."""

View File

@ -32,6 +32,7 @@ from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter
from _pytest.compat import _format_args
from _pytest.compat import _PytestWrapper
from _pytest.compat import final
from _pytest.compat import get_real_func
from _pytest.compat import get_real_method
from _pytest.compat import getfuncargnames
@ -730,6 +731,7 @@ class FixtureRequest:
return "<FixtureRequest for %r>" % (self.node)
@final
class SubRequest(FixtureRequest):
"""A sub request for handling getting a fixture from a test function/fixture."""
@ -796,6 +798,7 @@ def scope2index(scope: str, descr: str, where: Optional[str] = None) -> int:
)
@final
class FixtureLookupError(LookupError):
"""Could not return a requested fixture (missing or invalid)."""
@ -952,6 +955,7 @@ def _eval_scope_callable(
return result
@final
class FixtureDef(Generic[_FixtureValue]):
"""A container for a factory definition."""
@ -1161,6 +1165,7 @@ def wrap_function_to_error_out_if_called_directly(function, fixture_marker):
return result
@final
@attr.s(frozen=True)
class FixtureFunctionMarker:
scope = attr.ib(type="Union[_Scope, Callable[[str, Config], _Scope]]")

View File

@ -19,6 +19,7 @@ import pytest
from _pytest import nodes
from _pytest._io import TerminalWriter
from _pytest.capture import CaptureManager
from _pytest.compat import final
from _pytest.compat import nullcontext
from _pytest.config import _strtobool
from _pytest.config import Config
@ -339,6 +340,7 @@ class LogCaptureHandler(logging.StreamHandler):
raise
@final
class LogCaptureFixture:
"""Provides access and control of log capturing."""

View File

@ -21,6 +21,7 @@ import py
import _pytest._code
from _pytest import nodes
from _pytest.compat import final
from _pytest.compat import overload
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
@ -435,6 +436,7 @@ class _bestrelpath_cache(Dict[Path, str]):
return r
@final
class Session(nodes.FSCollector):
Interrupted = Interrupted
Failed = Failed

View File

@ -20,6 +20,7 @@ import attr
from .._code import getfslineno
from ..compat import ascii_escaped
from ..compat import final
from ..compat import NOTSET
from ..compat import NotSetType
from ..compat import overload
@ -199,6 +200,7 @@ class ParameterSet(
return argnames, parameters
@final
@attr.s(frozen=True)
class Mark:
#: Name of the mark.
@ -452,6 +454,7 @@ if TYPE_CHECKING:
...
@final
class MarkGenerator:
"""Factory for :class:`MarkDecorator` objects - exposed as
a ``pytest.mark`` singleton instance.
@ -525,6 +528,7 @@ MARK_GEN = MarkGenerator()
# TODO(py36): inherit from typing.MutableMapping[str, Any].
@final
class NodeKeywords(collections.abc.MutableMapping): # type: ignore[type-arg]
def __init__(self, node: "Node") -> None:
self.node = node

View File

@ -14,6 +14,7 @@ from typing import TypeVar
from typing import Union
import pytest
from _pytest.compat import final
from _pytest.compat import overload
from _pytest.fixtures import fixture
from _pytest.pathlib import Path
@ -110,6 +111,7 @@ class Notset:
notset = Notset()
@final
class MonkeyPatch:
"""Object returned by the ``monkeypatch`` fixture keeping a record of
setattr/item/env/syspath changes."""

View File

@ -28,6 +28,7 @@ import pytest
from _pytest import timing
from _pytest._code import Source
from _pytest.capture import _get_multicapture
from _pytest.compat import final
from _pytest.compat import overload
from _pytest.compat import TYPE_CHECKING
from _pytest.config import _PluggyPlugin
@ -597,6 +598,7 @@ class SysPathsSnapshot:
sys.path[:], sys.meta_path[:] = self.__saved
@final
class Testdir:
"""Temporary test directory with tools to test/run pytest itself.

View File

@ -37,6 +37,7 @@ from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter
from _pytest._io.saferepr import saferepr
from _pytest.compat import ascii_escaped
from _pytest.compat import final
from _pytest.compat import get_default_arg_names
from _pytest.compat import get_real_func
from _pytest.compat import getimfunc
@ -864,6 +865,7 @@ def hasnew(obj: object) -> bool:
return False
@final
class CallSpec2:
def __init__(self, metafunc: "Metafunc") -> None:
self.metafunc = metafunc
@ -924,6 +926,7 @@ class CallSpec2:
self.marks.extend(normalize_mark_list(marks))
@final
class Metafunc:
"""Objects passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook.

View File

@ -17,6 +17,7 @@ from typing import TypeVar
from typing import Union
import _pytest._code
from _pytest.compat import final
from _pytest.compat import overload
from _pytest.compat import STRING_TYPES
from _pytest.compat import TYPE_CHECKING
@ -699,6 +700,7 @@ def raises( # noqa: F811
raises.Exception = fail.Exception # type: ignore
@final
class RaisesContext(Generic[_E]):
def __init__(
self,

View File

@ -13,6 +13,7 @@ from typing import Tuple
from typing import TypeVar
from typing import Union
from _pytest.compat import final
from _pytest.compat import overload
from _pytest.compat import TYPE_CHECKING
from _pytest.fixtures import fixture
@ -228,6 +229,7 @@ class WarningsRecorder(warnings.catch_warnings):
self._entered = False
@final
class WarningsChecker(WarningsRecorder):
def __init__(
self,

View File

@ -26,6 +26,7 @@ from _pytest._code.code import ReprLocals
from _pytest._code.code import ReprTraceback
from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter
from _pytest.compat import final
from _pytest.compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.nodes import Collector
@ -225,6 +226,7 @@ def _report_unserialization_failure(
raise RuntimeError(stream.getvalue())
@final
class TestReport(BaseReport):
"""Basic test report object (also used for setup and teardown calls if
they fail)."""
@ -333,6 +335,7 @@ class TestReport(BaseReport):
)
@final
class CollectReport(BaseReport):
"""Collection report object."""

View File

@ -22,6 +22,7 @@ from _pytest import timing
from _pytest._code.code import ExceptionChainRepr
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr
from _pytest.compat import final
from _pytest.compat import TYPE_CHECKING
from _pytest.config.argparsing import Parser
from _pytest.nodes import Collector
@ -259,6 +260,7 @@ def call_runtest_hook(
TResult = TypeVar("TResult", covariant=True)
@final
@attr.s(repr=False)
class CallInfo(Generic[TResult]):
"""Result/Exception info a function invocation.

View File

@ -32,6 +32,7 @@ from _pytest import timing
from _pytest._code import ExceptionInfo
from _pytest._code.code import ExceptionRepr
from _pytest._io.wcwidth import wcswidth
from _pytest.compat import final
from _pytest.compat import order_preserving_dict
from _pytest.compat import TYPE_CHECKING
from _pytest.config import _PluggyPlugin
@ -309,6 +310,7 @@ class WarningReport:
return None
@final
class TerminalReporter:
def __init__(self, config: Config, file: Optional[TextIO] = None) -> None:
import _pytest.config

View File

@ -13,11 +13,13 @@ from .pathlib import LOCK_TIMEOUT
from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup
from .pathlib import Path
from _pytest.compat import final
from _pytest.config import Config
from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch
@final
@attr.s
class TempPathFactory:
"""Factory for temporary directories under the common base temp directory.
@ -103,6 +105,7 @@ class TempPathFactory:
return t
@final
@attr.s
class TempdirFactory:
"""Backward comptibility wrapper that implements :class:``py.path.local``

View File

@ -4,6 +4,7 @@ from typing import TypeVar
import attr
from _pytest.compat import final
from _pytest.compat import TYPE_CHECKING
if TYPE_CHECKING:
@ -16,36 +17,42 @@ class PytestWarning(UserWarning):
__module__ = "pytest"
@final
class PytestAssertRewriteWarning(PytestWarning):
"""Warning emitted by the pytest assert rewrite module."""
__module__ = "pytest"
@final
class PytestCacheWarning(PytestWarning):
"""Warning emitted by the cache plugin in various situations."""
__module__ = "pytest"
@final
class PytestConfigWarning(PytestWarning):
"""Warning emitted for configuration issues."""
__module__ = "pytest"
@final
class PytestCollectionWarning(PytestWarning):
"""Warning emitted when pytest is not able to collect a file or symbol in a module."""
__module__ = "pytest"
@final
class PytestDeprecationWarning(PytestWarning, DeprecationWarning):
"""Warning class for features that will be removed in a future version."""
__module__ = "pytest"
@final
class PytestExperimentalApiWarning(PytestWarning, FutureWarning):
"""Warning category used to denote experiments in pytest.
@ -64,6 +71,7 @@ class PytestExperimentalApiWarning(PytestWarning, FutureWarning):
)
@final
class PytestUnhandledCoroutineWarning(PytestWarning):
"""Warning emitted for an unhandled coroutine.
@ -75,6 +83,7 @@ class PytestUnhandledCoroutineWarning(PytestWarning):
__module__ = "pytest"
@final
class PytestUnknownMarkWarning(PytestWarning):
"""Warning emitted on use of unknown markers.
@ -87,6 +96,7 @@ class PytestUnknownMarkWarning(PytestWarning):
_W = TypeVar("_W", bound=PytestWarning)
@final
@attr.s
class UnformattedWarning(Generic[_W]):
"""A warning meant to be formatted during runtime.