From 902b310f159cfaddb9499e97a967778356964339 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Thu, 7 May 2020 14:40:59 -0700 Subject: [PATCH 1/6] Apply the NoPublicConstructor metaclass to Task and TrioToken These have never had a usable public constructor, so we might as well use the annotation so in case anyone tries they'll get a nice error message. --- trio/_core/_entry_queue.py | 3 ++- trio/_core/_run.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/trio/_core/_entry_queue.py b/trio/_core/_entry_queue.py index a8fd8e32bd..869f616b5e 100644 --- a/trio/_core/_entry_queue.py +++ b/trio/_core/_entry_queue.py @@ -4,6 +4,7 @@ import attr from .. import _core +from .._util import NoPublicConstructor from ._wakeup_socketpair import WakeupSocketpair __all__ = ["TrioToken"] @@ -123,7 +124,7 @@ def run_sync_soon(self, sync_fn, *args, idempotent=False): self.wakeup.wakeup_thread_and_signal_safe() -class TrioToken: +class TrioToken(metaclass=NoPublicConstructor): """An opaque object representing a single call to :func:`trio.run`. It has no public constructor; instead, see :func:`current_trio_token`. diff --git a/trio/_core/_run.py b/trio/_core/_run.py index 11d65750e6..5904e682fd 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -968,7 +968,7 @@ def __del__(self): @attr.s(eq=False, hash=False, repr=False) -class Task: +class Task(metaclass=NoPublicConstructor): _parent_nursery = attr.ib() coro = attr.ib() _runner = attr.ib() @@ -1353,7 +1353,7 @@ async def python_wrapper(orig_coro): LOCALS_KEY_KI_PROTECTION_ENABLED, system_task ) - task = Task( + task = Task._create( coro=coro, parent_nursery=nursery, runner=self, @@ -1489,7 +1489,7 @@ def current_trio_token(self): """ if self.trio_token is None: - self.trio_token = TrioToken(self.entry_queue) + self.trio_token = TrioToken._create(self.entry_queue) return self.trio_token ################ From d5929fc8dfa644b258b264912c49dcc2b3a17795 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Thu, 7 May 2020 14:44:30 -0700 Subject: [PATCH 2/6] Add SubclassingDeprecatedIn_v0_15_0 metaclass For when we want to switch to Final, but with a deprecation period first. --- trio/_util.py | 16 ++++++++++++++++ trio/tests/test_util.py | 12 +++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/trio/_util.py b/trio/_util.py index b50a58036e..5cc5319d93 100644 --- a/trio/_util.py +++ b/trio/_util.py @@ -9,6 +9,8 @@ import typing as t import threading +from ._deprecate import warn_deprecated + # There's a dependency loop here... _core is allowed to use this file (in fact # it's the *only* file in the main trio/ package it's allowed to use), but # ConflictDetector needs checkpoint so it also has to import @@ -221,6 +223,20 @@ def __new__(cls, name, bases, cls_namespace): return super().__new__(cls, name, bases, cls_namespace) +class SubclassingDeprecatedIn_v0_15_0(BaseMeta): + def __new__(cls, name, bases, cls_namespace): + for base in bases: + if isinstance(base, SubclassingDeprecatedIn_v0_15_0): + warn_deprecated( + f"subclassing {base.__module__}.{base.__qualname__}", + "0.15.0", + issue=1044, + instead="composition or delegation" + ) + break + return super().__new__(cls, name, bases, cls_namespace) + + class NoPublicConstructor(Final): """Metaclass that enforces a class to be final (i.e., subclass not allowed) and ensures a private constructor. diff --git a/trio/tests/test_util.py b/trio/tests/test_util.py index 90aec096b1..8369ea6da0 100644 --- a/trio/tests/test_util.py +++ b/trio/tests/test_util.py @@ -6,7 +6,7 @@ from .. import _core from .._util import ( signal_raise, ConflictDetector, is_main_thread, generic_function, Final, - NoPublicConstructor + NoPublicConstructor, SubclassingDeprecatedIn_v0_15_0 ) from ..testing import wait_all_tasks_blocked @@ -108,6 +108,16 @@ class SubClass(FinalClass): pass +def test_subclassing_deprecated_metaclass(): + class Blah(metaclass=SubclassingDeprecatedIn_v0_15_0): + pass + + with pytest.warns(trio.TrioDeprecationWarning): + + class Blah2(Blah): + pass + + def test_no_public_constructor_metaclass(): class SpecialClass(metaclass=NoPublicConstructor): pass From 3382551d6bb41090c31b3797329ea4b5bf85c36c Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Thu, 7 May 2020 16:16:09 -0700 Subject: [PATCH 3/6] Deprecate subclassing for most of Trio's public classes --- newsfragments/1044.removal.rst | 17 ++++++++++++++ trio/_core/_local.py | 4 +++- trio/_core/_parking_lot.py | 3 ++- trio/_core/_unbounded_queue.py | 3 ++- trio/_highlevel_generic.py | 6 ++++- trio/_highlevel_socket.py | 10 +++++--- trio/_path.py | 4 ++-- trio/_ssl.py | 8 ++++--- trio/_sync.py | 41 ++++++++++++++++++--------------- trio/_unix_pipes.py | 4 ++-- trio/_windows_pipes.py | 6 ++--- trio/testing/_memory_streams.py | 8 +++++-- trio/testing/_mock_clock.py | 3 ++- trio/testing/_sequencer.py | 2 +- 14 files changed, 79 insertions(+), 40 deletions(-) create mode 100644 newsfragments/1044.removal.rst diff --git a/newsfragments/1044.removal.rst b/newsfragments/1044.removal.rst new file mode 100644 index 0000000000..a1f3868263 --- /dev/null +++ b/newsfragments/1044.removal.rst @@ -0,0 +1,17 @@ +Most of the public classes that Trio exports – like `trio.Lock`, +`trio.SocketStream`, and so on – weren't designed with subclassing in +mind. And we've noticed that some users were trying to subclass them +anyway, and ending up with fragile code that we're likely to +accidentally break in the future, or else be stuck unable to make +changes for fear of breaking subclasses. + +There are also some classes that were explicitly designed to be +subclassed, like the ones in `trio.abc`. Subclassing these is still +supported. However, for all other classes, attempts to subclass will +now raise a deprecation warning, and in the future will raise an +error. + +If this causes problems for you, feel free to drop by our `chat room +`__ or file a bug, to discuss +alternatives or make a case for why some particular class should be +designed to support subclassing. diff --git a/trio/_core/_local.py b/trio/_core/_local.py index 7ff3757356..589f5f5a90 100644 --- a/trio/_core/_local.py +++ b/trio/_core/_local.py @@ -1,6 +1,8 @@ # Runvar implementations from . import _run +from .._util import SubclassingDeprecatedIn_v0_15_0 + __all__ = ["RunVar"] @@ -19,7 +21,7 @@ def __init__(self, var, value): self.redeemed = False -class RunVar: +class RunVar(metaclass=SubclassingDeprecatedIn_v0_15_0): """The run-local variant of a context variable. :class:`RunVar` objects are similar to context variable objects, diff --git a/trio/_core/_parking_lot.py b/trio/_core/_parking_lot.py index 696ff90917..0bfc12230b 100644 --- a/trio/_core/_parking_lot.py +++ b/trio/_core/_parking_lot.py @@ -75,6 +75,7 @@ from collections import OrderedDict from .. import _core +from .._util import SubclassingDeprecatedIn_v0_15_0 __all__ = ["ParkingLot"] @@ -87,7 +88,7 @@ class _ParkingLotStatistics: @attr.s(eq=False, hash=False) -class ParkingLot: +class ParkingLot(metaclass=SubclassingDeprecatedIn_v0_15_0): """A fair wait queue with cancellation and requeueing. This class encapsulates the tricky parts of implementing a wait diff --git a/trio/_core/_unbounded_queue.py b/trio/_core/_unbounded_queue.py index 642a4a25ac..2cb82d1fce 100644 --- a/trio/_core/_unbounded_queue.py +++ b/trio/_core/_unbounded_queue.py @@ -2,6 +2,7 @@ from .. import _core from .._deprecate import deprecated +from .._util import SubclassingDeprecatedIn_v0_15_0 __all__ = ["UnboundedQueue"] @@ -12,7 +13,7 @@ class _UnboundedQueueStats: tasks_waiting = attr.ib() -class UnboundedQueue: +class UnboundedQueue(metaclass=SubclassingDeprecatedIn_v0_15_0): """An unbounded queue suitable for certain unusual forms of inter-task communication. diff --git a/trio/_highlevel_generic.py b/trio/_highlevel_generic.py index 79a82d36d8..7cdc0c75d1 100644 --- a/trio/_highlevel_generic.py +++ b/trio/_highlevel_generic.py @@ -3,6 +3,8 @@ import trio from .abc import HalfCloseableStream +from trio._util import SubclassingDeprecatedIn_v0_15_0 + async def aclose_forcefully(resource): """Close an async resource or async generator immediately, without @@ -35,7 +37,9 @@ async def aclose_forcefully(resource): @attr.s(eq=False, hash=False) -class StapledStream(HalfCloseableStream): +class StapledStream( + HalfCloseableStream, metaclass=SubclassingDeprecatedIn_v0_15_0 +): """This class `staples `__ together two unidirectional streams to make single bidirectional stream. diff --git a/trio/_highlevel_socket.py b/trio/_highlevel_socket.py index 405375479c..133a9981a1 100644 --- a/trio/_highlevel_socket.py +++ b/trio/_highlevel_socket.py @@ -5,7 +5,7 @@ import trio from . import socket as tsocket -from ._util import ConflictDetector +from ._util import ConflictDetector, SubclassingDeprecatedIn_v0_15_0 from .abc import HalfCloseableStream, Listener __all__ = ["SocketStream", "SocketListener"] @@ -39,7 +39,9 @@ def _translate_socket_errors_to_stream_errors(): ) from exc -class SocketStream(HalfCloseableStream): +class SocketStream( + HalfCloseableStream, metaclass=SubclassingDeprecatedIn_v0_15_0 +): """An implementation of the :class:`trio.abc.HalfCloseableStream` interface based on a raw network socket. @@ -322,7 +324,9 @@ def getsockopt(self, level, option, buffersize=0): pass -class SocketListener(Listener[SocketStream]): +class SocketListener( + Listener[SocketStream], metaclass=SubclassingDeprecatedIn_v0_15_0 +): """A :class:`~trio.abc.Listener` that uses a listening socket to accept incoming connections as :class:`SocketStream` objects. diff --git a/trio/_path.py b/trio/_path.py index 2f34a38972..45fe0e4341 100644 --- a/trio/_path.py +++ b/trio/_path.py @@ -4,7 +4,7 @@ import pathlib import trio -from trio._util import async_wraps +from trio._util import async_wraps, SubclassingDeprecatedIn_v0_15_0 __all__ = ['Path'] @@ -77,7 +77,7 @@ async def wrapper(cls, *args, **kwargs): return wrapper -class AsyncAutoWrapperType(type): +class AsyncAutoWrapperType(SubclassingDeprecatedIn_v0_15_0): def __init__(cls, name, bases, attrs): super().__init__(name, bases, attrs) diff --git a/trio/_ssl.py b/trio/_ssl.py index c72e0fc3b0..12182c58af 100644 --- a/trio/_ssl.py +++ b/trio/_ssl.py @@ -158,7 +158,7 @@ from .abc import Stream, Listener from ._highlevel_generic import aclose_forcefully from . import _sync -from ._util import ConflictDetector +from ._util import ConflictDetector, SubclassingDeprecatedIn_v0_15_0 from ._deprecate import warn_deprecated ################################################################ @@ -224,7 +224,7 @@ def done(self): _State = _Enum("_State", ["OK", "BROKEN", "CLOSED"]) -class SSLStream(Stream): +class SSLStream(Stream, metaclass=SubclassingDeprecatedIn_v0_15_0): r"""Encrypted communication using SSL/TLS. :class:`SSLStream` wraps an arbitrary :class:`~trio.abc.Stream`, and @@ -882,7 +882,9 @@ async def wait_send_all_might_not_block(self): await self.transport_stream.wait_send_all_might_not_block() -class SSLListener(Listener[SSLStream]): +class SSLListener( + Listener[SSLStream], metaclass=SubclassingDeprecatedIn_v0_15_0 +): """A :class:`~trio.abc.Listener` for SSL/TLS-encrypted servers. :class:`SSLListener` wraps around another Listener, and converts diff --git a/trio/_sync.py b/trio/_sync.py index 73854c3cc0..d5a5d04372 100644 --- a/trio/_sync.py +++ b/trio/_sync.py @@ -7,6 +7,7 @@ from ._core import enable_ki_protection, ParkingLot from ._deprecate import deprecated +from ._util import SubclassingDeprecatedIn_v0_15_0 __all__ = [ "Event", @@ -19,7 +20,7 @@ @attr.s(repr=False, eq=False, hash=False) -class Event: +class Event(metaclass=SubclassingDeprecatedIn_v0_15_0): """A waitable boolean value useful for inter-task synchronization, inspired by :class:`threading.Event`. @@ -111,7 +112,7 @@ class _CapacityLimiterStatistics: @async_cm -class CapacityLimiter: +class CapacityLimiter(metaclass=SubclassingDeprecatedIn_v0_15_0): """An object for controlling access to a resource with limited capacity. Sometimes you need to put a limit on how many tasks can do something at @@ -363,7 +364,7 @@ def statistics(self): @async_cm -class Semaphore: +class Semaphore(metaclass=SubclassingDeprecatedIn_v0_15_0): """A `semaphore `__. A semaphore holds an integer value, which can be incremented by @@ -499,19 +500,7 @@ class _LockStatistics: @async_cm @attr.s(eq=False, hash=False, repr=False) -class Lock: - """A classic `mutex - `__. - - This is a non-reentrant, single-owner lock. Unlike - :class:`threading.Lock`, only the owner of the lock is allowed to release - it. - - A :class:`Lock` object can be used as an async context manager; it - blocks on entry but not on exit. - - """ - +class _LockImpl: _lot = attr.ib(factory=ParkingLot, init=False) _owner = attr.ib(default=None, init=False) @@ -606,7 +595,21 @@ def statistics(self): ) -class StrictFIFOLock(Lock): +class Lock(_LockImpl, metaclass=SubclassingDeprecatedIn_v0_15_0): + """A classic `mutex + `__. + + This is a non-reentrant, single-owner lock. Unlike + :class:`threading.Lock`, only the owner of the lock is allowed to release + it. + + A :class:`Lock` object can be used as an async context manager; it + blocks on entry but not on exit. + + """ + + +class StrictFIFOLock(_LockImpl, metaclass=SubclassingDeprecatedIn_v0_15_0): r"""A variant of :class:`Lock` where tasks are guaranteed to acquire the lock in strict first-come-first-served order. @@ -658,7 +661,7 @@ class StrictFIFOLock(Lock): :class:`StrictFIFOLock` guarantees that each task will send its data in the same order that the state machine generated it. - Currently, :class:`StrictFIFOLock` is simply an alias for :class:`Lock`, + Currently, :class:`StrictFIFOLock` is identical to :class:`Lock`, but (a) this may not always be true in the future, especially if Trio ever implements `more sophisticated scheduling policies `__, and (b) the above code @@ -676,7 +679,7 @@ class _ConditionStatistics: @async_cm -class Condition: +class Condition(metaclass=SubclassingDeprecatedIn_v0_15_0): """A classic `condition variable `__, similar to :class:`threading.Condition`. diff --git a/trio/_unix_pipes.py b/trio/_unix_pipes.py index 6a961d027d..f2afe8d2b1 100644 --- a/trio/_unix_pipes.py +++ b/trio/_unix_pipes.py @@ -2,7 +2,7 @@ import errno from ._abc import Stream -from ._util import ConflictDetector +from ._util import ConflictDetector, SubclassingDeprecatedIn_v0_15_0 import trio @@ -74,7 +74,7 @@ async def aclose(self): await trio.lowlevel.checkpoint() -class FdStream(Stream): +class FdStream(Stream, metaclass=SubclassingDeprecatedIn_v0_15_0): """ Represents a stream given the file descriptor to a pipe, TTY, etc. diff --git a/trio/_windows_pipes.py b/trio/_windows_pipes.py index 04bcdc7100..6af69f364a 100644 --- a/trio/_windows_pipes.py +++ b/trio/_windows_pipes.py @@ -1,6 +1,6 @@ from . import _core from ._abc import SendStream, ReceiveStream -from ._util import ConflictDetector +from ._util import ConflictDetector, Final from ._core._windows_cffi import _handle, raise_winerror, kernel32, ffi # XX TODO: don't just make this up based on nothing. @@ -37,7 +37,7 @@ def __del__(self): self._close() -class PipeSendStream(SendStream): +class PipeSendStream(SendStream, metaclass=Final): """Represents a send stream over a Windows named pipe that has been opened in OVERLAPPED mode. """ @@ -79,7 +79,7 @@ async def aclose(self): await self._handle_holder.aclose() -class PipeReceiveStream(ReceiveStream): +class PipeReceiveStream(ReceiveStream, metaclass=Final): """Represents a receive stream over an os.pipe object.""" def __init__(self, handle: int) -> None: self._handle_holder = _HandleHolder(handle) diff --git a/trio/testing/_memory_streams.py b/trio/testing/_memory_streams.py index d86e301888..184626d58b 100644 --- a/trio/testing/_memory_streams.py +++ b/trio/testing/_memory_streams.py @@ -83,7 +83,9 @@ async def get(self, max_bytes=None): return self._get_impl(max_bytes) -class MemorySendStream(SendStream): +class MemorySendStream( + SendStream, metaclass=_util.SubclassingDeprecatedIn_v0_15_0 +): """An in-memory :class:`~trio.abc.SendStream`. Args: @@ -198,7 +200,9 @@ def get_data_nowait(self, max_bytes=None): return self._outgoing.get_nowait(max_bytes) -class MemoryReceiveStream(ReceiveStream): +class MemoryReceiveStream( + ReceiveStream, metaclass=_util.SubclassingDeprecatedIn_v0_15_0 +): """An in-memory :class:`~trio.abc.ReceiveStream`. Args: diff --git a/trio/testing/_mock_clock.py b/trio/testing/_mock_clock.py index c3cf863392..2e0667ab94 100644 --- a/trio/testing/_mock_clock.py +++ b/trio/testing/_mock_clock.py @@ -3,6 +3,7 @@ from .. import _core from .._abc import Clock +from .._util import SubclassingDeprecatedIn_v0_15_0 __all__ = ["MockClock"] @@ -14,7 +15,7 @@ # Prior art: # https://twistedmatrix.com/documents/current/api/twisted.internet.task.Clock.html # https://github.com/ztellman/manifold/issues/57 -class MockClock(Clock): +class MockClock(Clock, metaclass=SubclassingDeprecatedIn_v0_15_0): """A user-controllable clock suitable for writing tests. Args: diff --git a/trio/testing/_sequencer.py b/trio/testing/_sequencer.py index 05b7560eec..1ff190e985 100644 --- a/trio/testing/_sequencer.py +++ b/trio/testing/_sequencer.py @@ -14,7 +14,7 @@ @attr.s(eq=False, hash=False) -class Sequencer: +class Sequencer(metaclass=_util.SubclassingDeprecatedIn_v0_15_0): """A convenience class for forcing code in different tasks to run in an explicit linear order. From db179cbfa44f9c95e590b430ddc17ead350344b0 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Thu, 7 May 2020 17:54:25 -0700 Subject: [PATCH 4/6] Add a test to confirm that all public classes are Final, or have a good excuse --- trio/tests/test_exports.py | 56 +++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/trio/tests/test_exports.py b/trio/tests/test_exports.py index 6085182a0e..af34070267 100644 --- a/trio/tests/test_exports.py +++ b/trio/tests/test_exports.py @@ -1,6 +1,8 @@ import sys import importlib import types +import inspect +import enum import pytest @@ -8,6 +10,7 @@ import trio.testing from .. import _core +from .. import _util def test_core_is_properly_reexported(): @@ -28,25 +31,26 @@ def test_core_is_properly_reexported(): assert found == 1 -def public_namespaces(module): - yield module.__name__ - for name, value in module.__dict__.items(): +def public_modules(module): + yield module + for name, class_ in module.__dict__.items(): if name.startswith("_"): continue - if not isinstance(value, types.ModuleType): + if not isinstance(class_, types.ModuleType): continue - if not value.__name__.startswith(module.__name__): + if not class_.__name__.startswith(module.__name__): continue - if value is module: + if class_ is module: continue # We should rename the trio.tests module (#274), but until then we use # a special-case hack: - if value.__name__ == "trio.tests": + if class_.__name__ == "trio.tests": continue - yield from public_namespaces(value) + yield from public_modules(class_) -NAMESPACES = list(public_namespaces(trio)) +PUBLIC_MODULES = list(public_modules(trio)) +PUBLIC_MODULE_NAMES = [m.__name__ for m in PUBLIC_MODULES] # It doesn't make sense for downstream redistributors to run this test, since @@ -64,7 +68,7 @@ def public_namespaces(module): # https://github.com/PyCQA/astroid/issues/681 "ignore:the imp module is deprecated.*:DeprecationWarning" ) -@pytest.mark.parametrize("modname", NAMESPACES) +@pytest.mark.parametrize("modname", PUBLIC_MODULE_NAMES) @pytest.mark.parametrize("tool", ["pylint", "jedi"]) def test_static_tool_sees_all_symbols(tool, modname): module = importlib.import_module(modname) @@ -106,3 +110,35 @@ def no_underscores(symbols): for name in sorted(missing_names): print(" {}".format(name)) assert False + + +def test_classes_are_final(): + for module in PUBLIC_MODULES: + for name, class_ in module.__dict__.items(): + if not isinstance(class_, type): + continue + if name.startswith("_"): + continue + + # Abstract classes can be subclassed, because that's the whole + # point of ABCs + if inspect.isabstract(class_): + continue + # Exceptions are allowed to be subclassed, because exception + # subclassing isn't used to inherit behavior. + if issubclass(class_, BaseException): + continue + # These are classes that are conceptually abstract, but + # inspect.isabstract returns False for boring reasons. + if class_ in {trio.abc.Instrument, trio.socket.SocketType}: + continue + # Enums have their own metaclass, so we can't use our metaclasses. + # And I don't think there's a lot of risk from people subclassing + # enums... + if issubclass(class_, enum.Enum): + continue + # ... insert other special cases here ... + + assert isinstance( + class_, (_util.Final, _util.SubclassingDeprecatedIn_v0_15_0) + ) From 7e10cd547aab92e4f650e563375b75c734f6055d Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Thu, 7 May 2020 21:57:42 -0700 Subject: [PATCH 5/6] Don't try to link to trio.abc --- newsfragments/1044.removal.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/1044.removal.rst b/newsfragments/1044.removal.rst index a1f3868263..7939eafc00 100644 --- a/newsfragments/1044.removal.rst +++ b/newsfragments/1044.removal.rst @@ -6,7 +6,7 @@ accidentally break in the future, or else be stuck unable to make changes for fear of breaking subclasses. There are also some classes that were explicitly designed to be -subclassed, like the ones in `trio.abc`. Subclassing these is still +subclassed, like the ones in ``trio.abc``. Subclassing these is still supported. However, for all other classes, attempts to subclass will now raise a deprecation warning, and in the future will raise an error. From d535bb527717d7573de38ebed47dae522e195f88 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Thu, 7 May 2020 22:01:52 -0700 Subject: [PATCH 6/6] Fix trio.Lock docs --- docs/source/reference-core.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/source/reference-core.rst b/docs/source/reference-core.rst index d868f9b1e7..b5f6d7c4d8 100644 --- a/docs/source/reference-core.rst +++ b/docs/source/reference-core.rst @@ -1444,8 +1444,14 @@ don't have any special access to Trio's internals.) .. autoclass:: Semaphore :members: +.. We have to use :inherited-members: here because all the actual lock + methods are stashed in _LockImpl. Weird side-effect of having both + Lock and StrictFIFOLock, but wanting both to be marked Final so + neither can inherit from the other. + .. autoclass:: Lock :members: + :inherited-members: .. autoclass:: StrictFIFOLock :members: