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 safeformat
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest.compat import ATTRS_EQ_FIELD from _pytest.compat import ATTRS_EQ_FIELD
from _pytest.compat import final
from _pytest.compat import get_real_func from _pytest.compat import get_real_func
from _pytest.compat import overload from _pytest.compat import overload
from _pytest.compat import TYPE_CHECKING from _pytest.compat import TYPE_CHECKING
@ -414,6 +415,7 @@ co_equal = compile(
_E = TypeVar("_E", bound=BaseException, covariant=True) _E = TypeVar("_E", bound=BaseException, covariant=True)
@final
@attr.s(repr=False) @attr.s(repr=False)
class ExceptionInfo(Generic[_E]): class ExceptionInfo(Generic[_E]):
"""Wraps sys.exc_info() objects and offers help for navigating the traceback.""" """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 typing import TextIO
from .wcwidth import wcswidth from .wcwidth import wcswidth
from _pytest.compat import final
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py. # 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: class TerminalWriter:
_esctable = dict( _esctable = dict(
black=30, black=30,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,7 @@
from _pytest.compat import final
@final
class UsageError(Exception): class UsageError(Exception):
"""Error in pytest usage or invocation.""" """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._io import TerminalWriter
from _pytest.compat import _format_args from _pytest.compat import _format_args
from _pytest.compat import _PytestWrapper from _pytest.compat import _PytestWrapper
from _pytest.compat import final
from _pytest.compat import get_real_func from _pytest.compat import get_real_func
from _pytest.compat import get_real_method from _pytest.compat import get_real_method
from _pytest.compat import getfuncargnames from _pytest.compat import getfuncargnames
@ -730,6 +731,7 @@ class FixtureRequest:
return "<FixtureRequest for %r>" % (self.node) return "<FixtureRequest for %r>" % (self.node)
@final
class SubRequest(FixtureRequest): class SubRequest(FixtureRequest):
"""A sub request for handling getting a fixture from a test function/fixture.""" """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): class FixtureLookupError(LookupError):
"""Could not return a requested fixture (missing or invalid).""" """Could not return a requested fixture (missing or invalid)."""
@ -952,6 +955,7 @@ def _eval_scope_callable(
return result return result
@final
class FixtureDef(Generic[_FixtureValue]): class FixtureDef(Generic[_FixtureValue]):
"""A container for a factory definition.""" """A container for a factory definition."""
@ -1161,6 +1165,7 @@ def wrap_function_to_error_out_if_called_directly(function, fixture_marker):
return result return result
@final
@attr.s(frozen=True) @attr.s(frozen=True)
class FixtureFunctionMarker: class FixtureFunctionMarker:
scope = attr.ib(type="Union[_Scope, Callable[[str, Config], _Scope]]") scope = attr.ib(type="Union[_Scope, Callable[[str, Config], _Scope]]")

View File

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

View File

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

View File

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

View File

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

View File

@ -28,6 +28,7 @@ import pytest
from _pytest import timing from _pytest import timing
from _pytest._code import Source from _pytest._code import Source
from _pytest.capture import _get_multicapture from _pytest.capture import _get_multicapture
from _pytest.compat import final
from _pytest.compat import overload from _pytest.compat import overload
from _pytest.compat import TYPE_CHECKING from _pytest.compat import TYPE_CHECKING
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
@ -597,6 +598,7 @@ class SysPathsSnapshot:
sys.path[:], sys.meta_path[:] = self.__saved sys.path[:], sys.meta_path[:] = self.__saved
@final
class Testdir: class Testdir:
"""Temporary test directory with tools to test/run pytest itself. """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 import TerminalWriter
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest.compat import ascii_escaped from _pytest.compat import ascii_escaped
from _pytest.compat import final
from _pytest.compat import get_default_arg_names from _pytest.compat import get_default_arg_names
from _pytest.compat import get_real_func from _pytest.compat import get_real_func
from _pytest.compat import getimfunc from _pytest.compat import getimfunc
@ -864,6 +865,7 @@ def hasnew(obj: object) -> bool:
return False return False
@final
class CallSpec2: class CallSpec2:
def __init__(self, metafunc: "Metafunc") -> None: def __init__(self, metafunc: "Metafunc") -> None:
self.metafunc = metafunc self.metafunc = metafunc
@ -924,6 +926,7 @@ class CallSpec2:
self.marks.extend(normalize_mark_list(marks)) self.marks.extend(normalize_mark_list(marks))
@final
class Metafunc: class Metafunc:
"""Objects passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook. """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 from typing import Union
import _pytest._code import _pytest._code
from _pytest.compat import final
from _pytest.compat import overload from _pytest.compat import overload
from _pytest.compat import STRING_TYPES from _pytest.compat import STRING_TYPES
from _pytest.compat import TYPE_CHECKING from _pytest.compat import TYPE_CHECKING
@ -699,6 +700,7 @@ def raises( # noqa: F811
raises.Exception = fail.Exception # type: ignore raises.Exception = fail.Exception # type: ignore
@final
class RaisesContext(Generic[_E]): class RaisesContext(Generic[_E]):
def __init__( def __init__(
self, self,

View File

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

View File

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

View File

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

View File

@ -32,6 +32,7 @@ from _pytest import timing
from _pytest._code import ExceptionInfo from _pytest._code import ExceptionInfo
from _pytest._code.code import ExceptionRepr from _pytest._code.code import ExceptionRepr
from _pytest._io.wcwidth import wcswidth from _pytest._io.wcwidth import wcswidth
from _pytest.compat import final
from _pytest.compat import order_preserving_dict from _pytest.compat import order_preserving_dict
from _pytest.compat import TYPE_CHECKING from _pytest.compat import TYPE_CHECKING
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
@ -309,6 +310,7 @@ class WarningReport:
return None return None
@final
class TerminalReporter: class TerminalReporter:
def __init__(self, config: Config, file: Optional[TextIO] = None) -> None: def __init__(self, config: Config, file: Optional[TextIO] = None) -> None:
import _pytest.config 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
from .pathlib import make_numbered_dir_with_cleanup from .pathlib import make_numbered_dir_with_cleanup
from .pathlib import Path from .pathlib import Path
from _pytest.compat import final
from _pytest.config import Config from _pytest.config import Config
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
@final
@attr.s @attr.s
class TempPathFactory: class TempPathFactory:
"""Factory for temporary directories under the common base temp directory. """Factory for temporary directories under the common base temp directory.
@ -103,6 +105,7 @@ class TempPathFactory:
return t return t
@final
@attr.s @attr.s
class TempdirFactory: class TempdirFactory:
"""Backward comptibility wrapper that implements :class:``py.path.local`` """Backward comptibility wrapper that implements :class:``py.path.local``

View File

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