diff --git a/.gitignore b/.gitignore index fe7bde9..8d49463 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ /debian/itchcraft.*.debhelper /debian/itchcraft.substvars /dist/ -/doc/sphinx/autoapi/ +/doc/sphinx/api/ /.pc/ /.venv/ *.egg-info diff --git a/README.md b/README.md index b562dc2..fbd76ac 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ See or `man itchcraft` for details. You can also browse the -[API reference on Read the Docs](https://itchcraft.readthedocs.io/en/stable/autoapi/itchcraft/) +[API reference on Read the Docs](https://itchcraft.readthedocs.io/en/stable/api/itchcraft/) for details. ## Contributing to Itchcraft diff --git a/doc/sphinx/conf.py b/doc/sphinx/conf.py index bbc731b..d1fff13 100644 --- a/doc/sphinx/conf.py +++ b/doc/sphinx/conf.py @@ -10,9 +10,12 @@ # type: ignore project = 'Itchcraft' +copyright = '2024 Claudia Pellegrino' executable = 'itchcraft' author = 'Claudia Pellegrino ' -description = 'Tech demo for interfacing with heat-based USB insect bite healers' +description = ( + 'Tech demo for interfacing with heat-based USB insect bite healers' +) # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -33,26 +36,38 @@ 'special-members', 'imported-members', ] +autoapi_root = 'api' autoapi_type = 'python' autodoc_typehints = 'description' html_theme = 'sphinx_rtd_theme' +python_display_short_literal_types = True +python_use_unqualified_type_names = True + myst_enable_extensions = [ 'deflist', ] + def skip_module(app, what, name, obj, skip, options): - if what != 'module': - return skip - if name in [ - 'itchcraft.__main__', - 'itchcraft.cli', - 'itchcraft.fire_workarounds', - 'itchcraft.version', - 'itchcraft.settings', - ]: - return True + if what == 'data': + return skip or name.endswith('.logger') + if what == 'method': + return skip or name.endswith('.__str__') + if what == 'module': + return ( + skip + or name + in [ + 'itchcraft.__main__', + 'itchcraft.cli', + 'itchcraft.fire_workarounds', + 'itchcraft.version', + 'itchcraft.settings', + ] + or obj.obj['relative_path'].startswith('itchcraft/stubs') + ) return skip @@ -67,6 +82,7 @@ def setup(sphinx): '**/itchcraft/fire_workarounds/**', '**/itchcraft/version/**', '**/itchcraft/settings/**', + '**/itchcraft/stubs/**', ] # Man page output diff --git a/doc/sphinx/index.rst b/doc/sphinx/index.rst index 302f290..ada4310 100644 --- a/doc/sphinx/index.rst +++ b/doc/sphinx/index.rst @@ -6,7 +6,7 @@ Documentation for Itchcraft :caption: Contents usage - autoapi/index + api/index .. include:: ../../README.md :parser: myst_parser.sphinx_ diff --git a/itchcraft/api.py b/itchcraft/api.py index cd5fa8a..c645c9d 100644 --- a/itchcraft/api.py +++ b/itchcraft/api.py @@ -23,7 +23,7 @@ # pylint: disable=too-few-public-methods class Api: - """Tech demo for interfacing with heat-based USB insect bite healers""" + """Tech demo for interfacing with heat-based USB insect bite healers.""" # pylint: disable=no-self-use def info(self) -> None: diff --git a/itchcraft/backend.py b/itchcraft/backend.py index c2d392c..c75cf1e 100644 --- a/itchcraft/backend.py +++ b/itchcraft/backend.py @@ -41,7 +41,11 @@ def serial_number(self) -> Optional[str]: class UsbBulkTransferDevice(BulkTransferDevice): - """USB device with two bulk transfer endpoints.""" + """USB device with two bulk transfer endpoints. + + :param device: + the PyUSB device with which to initiate the bulk transfer. + """ MAX_RESPONSE_LENGTH = 12 diff --git a/itchcraft/device.py b/itchcraft/device.py index f9bf7d0..1e1ccca 100644 --- a/itchcraft/device.py +++ b/itchcraft/device.py @@ -98,7 +98,19 @@ def from_usb_device( usb_device: usb.core.Device, support_statement: SupportStatement, ) -> BiteHealerMetadata: - """Creates a metadata object from a USB device.""" + """Creates a metadata object from a USB device. + + :param usb_device: + the PyUSB device to be queried for metadata. + + :param support_statement: + Describes the level of support that Itchcraft offers for + `usb_device`. + + :return: + a metadata object that unifies info from both the PyUSB device + and `support_statement`. + """ def try_get_usb_attribute( name: Literal['product', 'serial_number'], diff --git a/itchcraft/errors.py b/itchcraft/errors.py index 7971f23..f962cbb 100644 --- a/itchcraft/errors.py +++ b/itchcraft/errors.py @@ -2,7 +2,7 @@ class CliError(Exception): - """An user-facing error message.""" + """A user-facing error message.""" class BackendInitializationError(Exception): diff --git a/itchcraft/heat_it.py b/itchcraft/heat_it.py index 2ac4c16..f86ed81 100644 --- a/itchcraft/heat_it.py +++ b/itchcraft/heat_it.py @@ -23,7 +23,11 @@ class HeatItDevice(BiteHealer): - """A “heat it” bite healer, configured over USB.""" + """A “heat it” bite healer, configured over USB. + + :param device: + the backend object to which to delegate the USB bulk transfer. + """ device: BulkTransferDevice @@ -43,6 +47,9 @@ def get_status(self) -> bytes: def msg_start_heating(self, preferences: Preferences) -> bytes: """Issues a `MSG_START_HEATING` command and returns the response. + + :param preferences: + how the user wants the device to be configured. """ def duration_code() -> int: @@ -94,7 +101,11 @@ def self_test(self) -> None: logger.debug('Response: %s', self.get_status().hex(' ')) def start_with_preferences(self, preferences: Preferences) -> None: - """Tells the device to start heating up.""" + """Tells the device to start heating up. + + :param preferences: + how the user wants the device to be configured. + """ logger.debug( 'Response: %s', self.msg_start_heating(preferences).hex(' ') ) diff --git a/itchcraft/prefs.py b/itchcraft/prefs.py index 961f316..e73bb82 100644 --- a/itchcraft/prefs.py +++ b/itchcraft/prefs.py @@ -7,11 +7,46 @@ from .errors import CliError -E = TypeVar('E', bound=Enum) +_E = TypeVar('_E', bound=Enum) -# If a CLI switch is backed by an enum, then allow the enum to stand in -# for that switch -CliEnum = Union[str, E] +CliEnum = Union[str, _E] +"""Helper union type consisting of an :py:class:`~enum.Enum` and its stringified values. + +If a command line switch is backed by an `Enum`, then allow the enum +to stand in for that switch. + +For example, if you have the following `Enum` class: + +.. code:: python + + from enum import Enum + + class Widget(Enum): + FOO = 1 + BAR = 2 + +and an associated command line switch ``--widget``, which can take the +form of either ``--widget foo`` and ``--widget bar``, then +`CliEnum[Widget]` means that the four values ``Widget.FOO``, +``Widget.BAR``, ``"foo"``, and ``"bar"`` should be accepted. + +`Widget.FOO` is equivalent to ``"foo"``, while `Widget.BAR` stands in +for ``"bar"``: + +.. code:: python + + from itchcraft.prefs import CliEnum, parse + + def frob(widget: CliEnum[Widget]) -> None: + real_widget: Widget = parse(widget, Widget) + ... + + frob("foo") + frob("bar") + frob(Widget.FOO) # same as frob("foo") + frob(Widget.BAR) # same as frob("bar") + +""" class SkinSensitivity(Enum): @@ -66,28 +101,34 @@ def __str__(self) -> str: ) -def default(enum_type: type[E]) -> str: - """Returns the default preference for a given Enum type. +def default(enum_type: type[_E]) -> str: + """Returns the default preference for a given :py:class:`~enum.Enum` type. :param enum_type: - Enum type which exists as an attribute in Preferences and - whose corresponding attribute name is equal to the type name - converted to snake case. + Enum type which exists as an attribute in + :py:class:`.Preferences` and whose corresponding attribute name + is equal to the type name converted to snake case. """ - default_value: E = getattr(Preferences, _snake_case_name(enum_type)) + default_value: _E = getattr( + Preferences, _snake_case_name(enum_type) + ) return default_value.name.lower() # pylint: disable=raise-missing-from -def parse(value: CliEnum[E], enum_type: type[E]) -> E: - """Parses a given value into an Enum if it isn’t one yet. - Returns the value itself if it’s already an Enum. +def parse(value: CliEnum[_E], enum_type: type[_E]) -> _E: + """Parses a given value into an :py:class:`~enum.Enum` if it isn’t one yet. :param value: - an Enum value or a corresponding name, written in lower case. + an `Enum` value or a corresponding name, written in lower case. :param enum_type: - the type of the Enum to parse into. + the type of the `Enum` to parse into. + + :return: + an instance of `enum_type` that represents `value`. + If `value` is already an instance of `enum_type`, then the + return value is `value` itself. """ if isinstance(value, enum_type): return value diff --git a/itchcraft/start.py b/itchcraft/start.py index e45db2a..972f872 100644 --- a/itchcraft/start.py +++ b/itchcraft/start.py @@ -13,11 +13,10 @@ def start_with_preferences(preferences: Preferences) -> None: - """Activates (i.e. heats up) a connected USB bite healer for - demonstration purposes. + """Activates (i.e. heats up) a connected USB bite healer for demonstration purposes. :param preferences: - User preferences for the settings of the bite healer. + how the user wants the device to be configured. """ logger.warning('This app is only a tech demo') logger.warning('and NOT for medical use.') diff --git a/itchcraft/support.py b/itchcraft/support.py index 3745d20..8febf94 100644 --- a/itchcraft/support.py +++ b/itchcraft/support.py @@ -1,4 +1,4 @@ -"""Declaring supported bite healers""" +"""Database of supported bite healers.""" from collections.abc import Iterator from contextlib import AbstractContextManager, contextmanager @@ -14,33 +14,45 @@ @dataclass(frozen=True) class SupportStatement: - """Statement that establishes whether or not a given combination + """Describes the level of support that Itchcraft offers for a given device. + + A metadata object that establishes whether or not a given combination of vendor ID (VID) and product id (PID) is a supported bite healer, and which model it is.""" vid: int - """USB vendor ID""" + """The USB vendor ID.""" pid: int - """USB product ID""" + """The USB product ID.""" vendor_name: str - """Canonical vendor name from Itchcraft’s point of view""" + """The canonical vendor name from Itchcraft’s point of view.""" product_name: str - """Canonical product name from Itchcraft’s point of view""" + """The canonical product name from Itchcraft’s point of view.""" supported: bool = True - """Whether or not Itchcraft supports this model""" + """Whether or not Itchcraft supports this model.""" comment: Optional[str] = None - """Additional comments on the support status of this model""" + """Additional comments on the support status of this model.""" connection_supplier: Optional[ Callable[[usb.core.Device], AbstractContextManager[BiteHealer]] ] = None - """Callable that establishes a connection to this model""" + """An optional :py:class:`~typing.Callable` that, when invoked, + establishes a connection to an attached device of this model. + + :param usb_device: + a PyUSB device to which to connect. + + :return: + a context manager representing a :py:class:`~.types.BiteHealer`. + """ class VidPid(NamedTuple): """Tuple of USB vendor ID (VID) and product ID (PID).""" vid: int + """The USB vendor ID.""" pid: int + """The USB product ID.""" def __str__(self) -> str: return ( @@ -174,3 +186,4 @@ def _heat_it_device( """, ), ] +"""Hard-coded database of support statements for various bite healer models.""" diff --git a/itchcraft/types/__init__.py b/itchcraft/types/__init__.py index 044bf2b..4545816 100644 --- a/itchcraft/types/__init__.py +++ b/itchcraft/types/__init__.py @@ -8,6 +8,8 @@ SizedPayload = Union[bytes, Collection[int]] +"""Helper union type consisting of :py:class:`bytes` and :py:class:`collections.abc.Collection[int]`. +""" # pylint: disable=line-too-long class BiteHealer(ABC): @@ -20,4 +22,8 @@ def self_test(self) -> None: @abstractmethod def start_with_preferences(self, preferences: Preferences) -> None: - """Tells the device to start heating up.""" + """Tells the device to start heating up. + + :param preferences: + how the user wants the device to be configured. + """