Skip to content

Commit

Permalink
Mark some public and to-be-public classes as @final
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bluetech committed Sep 21, 2020
1 parent cdfdb3a commit 83bd4d6
Show file tree
Hide file tree
Showing 22 changed files with 78 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/_pytest/_code/code.py
Expand Up @@ -38,6 +38,7 @@
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
Expand Down Expand Up @@ -414,6 +415,7 @@ def recursionindex(self) -> Optional[int]:
_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."""
Expand Down
2 changes: 2 additions & 0 deletions src/_pytest/_io/terminalwriter.py
Expand Up @@ -7,6 +7,7 @@
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.
Expand Down Expand Up @@ -36,6 +37,7 @@ def should_do_markup(file: TextIO) -> bool:
)


@final
class TerminalWriter:
_esctable = dict(
black=30,
Expand Down
2 changes: 2 additions & 0 deletions src/_pytest/cacheprovider.py
Expand Up @@ -21,6 +21,7 @@
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
Expand Down Expand Up @@ -50,6 +51,7 @@
"""


@final
@attr.s
class Cache:
_cachedir = attr.ib(type=Path, repr=False)
Expand Down
2 changes: 2 additions & 0 deletions src/_pytest/capture.py
Expand Up @@ -17,6 +17,7 @@
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
Expand Down Expand Up @@ -498,6 +499,7 @@ def writeorg(self, data):
# 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`."""
Expand Down
16 changes: 15 additions & 1 deletion src/_pytest/compat.py
Expand Up @@ -19,7 +19,6 @@

import attr

from _pytest._io.saferepr import saferepr
from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -357,6 +358,19 @@ def overload(f): # noqa: F811
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:
Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/config/__init__.py
Expand Up @@ -43,6 +43,7 @@
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
Expand Down Expand Up @@ -76,6 +77,7 @@
hookspec = HookspecMarker("pytest")


@final
class ExitCode(enum.IntEnum):
"""Encodes the valid exit codes by pytest.
Expand Down Expand Up @@ -322,6 +324,7 @@ def _prepareconfig(
raise


@final
class PytestPluginManager(PluginManager):
"""A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with
additional pytest-specific functionality:
Expand Down Expand Up @@ -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.
Expand All @@ -825,6 +829,7 @@ class Config:
invocation.
"""

@final
@attr.s(frozen=True)
class InvocationParams:
"""Holds parameters passed during :func:`pytest.main`.
Expand Down
2 changes: 2 additions & 0 deletions src/_pytest/config/argparsing.py
Expand Up @@ -16,6 +16,7 @@
import py

import _pytest._io
from _pytest.compat import final
from _pytest.compat import TYPE_CHECKING
from _pytest.config.exceptions import UsageError

Expand All @@ -26,6 +27,7 @@
FILE_OR_DIR = "file_or_dir"


@final
class Parser:
"""Parser for command line arguments and ini-file values.
Expand Down
4 changes: 4 additions & 0 deletions src/_pytest/config/exceptions.py
@@ -1,3 +1,7 @@
from _pytest.compat import final


@final
class UsageError(Exception):
"""Error in pytest usage or invocation."""

Expand Down
5 changes: 5 additions & 0 deletions src/_pytest/fixtures.py
Expand Up @@ -32,6 +32,7 @@
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
Expand Down Expand Up @@ -730,6 +731,7 @@ def __repr__(self) -> str:
return "<FixtureRequest for %r>" % (self.node)


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

Expand Down Expand Up @@ -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)."""

Expand Down Expand Up @@ -952,6 +955,7 @@ def _eval_scope_callable(
return result


@final
class FixtureDef(Generic[_FixtureValue]):
"""A container for a factory definition."""

Expand Down Expand Up @@ -1161,6 +1165,7 @@ def result(*args, **kwargs):
return result


@final
@attr.s(frozen=True)
class FixtureFunctionMarker:
scope = attr.ib(type="Union[_Scope, Callable[[str, Config], _Scope]]")
Expand Down
2 changes: 2 additions & 0 deletions src/_pytest/logging.py
Expand Up @@ -19,6 +19,7 @@
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
Expand Down Expand Up @@ -339,6 +340,7 @@ def handleError(self, record: logging.LogRecord) -> None:
raise


@final
class LogCaptureFixture:
"""Provides access and control of log capturing."""

Expand Down
2 changes: 2 additions & 0 deletions src/_pytest/main.py
Expand Up @@ -21,6 +21,7 @@

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
Expand Down Expand Up @@ -435,6 +436,7 @@ def __missing__(self, path: Path) -> str:
return r


@final
class Session(nodes.FSCollector):
Interrupted = Interrupted
Failed = Failed
Expand Down
4 changes: 4 additions & 0 deletions src/_pytest/mark/structures.py
Expand Up @@ -20,6 +20,7 @@

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
Expand Down Expand Up @@ -199,6 +200,7 @@ def _for_parametrize(
return argnames, parameters


@final
@attr.s(frozen=True)
class Mark:
#: Name of the mark.
Expand Down Expand Up @@ -452,6 +454,7 @@ def __call__( # type: ignore[override]
...


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


# 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
Expand Down
2 changes: 2 additions & 0 deletions src/_pytest/monkeypatch.py
Expand Up @@ -14,6 +14,7 @@
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
Expand Down Expand Up @@ -110,6 +111,7 @@ def __repr__(self) -> str:
notset = Notset()


@final
class MonkeyPatch:
"""Object returned by the ``monkeypatch`` fixture keeping a record of
setattr/item/env/syspath changes."""
Expand Down
2 changes: 2 additions & 0 deletions src/_pytest/pytester.py
Expand Up @@ -28,6 +28,7 @@
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
Expand Down Expand Up @@ -597,6 +598,7 @@ def restore(self) -> None:
sys.path[:], sys.meta_path[:] = self.__saved


@final
class Testdir:
"""Temporary test directory with tools to test/run pytest itself.
Expand Down
3 changes: 3 additions & 0 deletions src/_pytest/python.py
Expand Up @@ -37,6 +37,7 @@
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
Expand Down Expand Up @@ -864,6 +865,7 @@ def hasnew(obj: object) -> bool:
return False


@final
class CallSpec2:
def __init__(self, metafunc: "Metafunc") -> None:
self.metafunc = metafunc
Expand Down Expand Up @@ -924,6 +926,7 @@ def setmulti2(
self.marks.extend(normalize_mark_list(marks))


@final
class Metafunc:
"""Objects passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook.
Expand Down
2 changes: 2 additions & 0 deletions src/_pytest/python_api.py
Expand Up @@ -17,6 +17,7 @@
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
Expand Down Expand Up @@ -699,6 +700,7 @@ def raises( # noqa: F811
raises.Exception = fail.Exception # type: ignore


@final
class RaisesContext(Generic[_E]):
def __init__(
self,
Expand Down
2 changes: 2 additions & 0 deletions src/_pytest/recwarn.py
Expand Up @@ -13,6 +13,7 @@
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
Expand Down Expand Up @@ -228,6 +229,7 @@ def __exit__(
self._entered = False


@final
class WarningsChecker(WarningsRecorder):
def __init__(
self,
Expand Down
3 changes: 3 additions & 0 deletions src/_pytest/reports.py
Expand Up @@ -26,6 +26,7 @@
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
Expand Down Expand Up @@ -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)."""
Expand Down Expand Up @@ -333,6 +335,7 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
)


@final
class CollectReport(BaseReport):
"""Collection report object."""

Expand Down
2 changes: 2 additions & 0 deletions src/_pytest/runner.py
Expand Up @@ -22,6 +22,7 @@
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
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 83bd4d6

Please sign in to comment.