Update docs for the new module attributes has/get feature (#1301)

This commit is contained in:
Steven B. 2024-11-26 08:38:20 +00:00 committed by GitHub
parent fe53cd7d9c
commit cb4e28394d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 70 additions and 34 deletions

View File

@ -64,9 +64,9 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from typing import NamedTuple from typing import Annotated, NamedTuple
from ..module import Module from ..module import FeatureAttribute, Module
@dataclass @dataclass
@ -129,7 +129,7 @@ class Light(Module, ABC):
@property @property
@abstractmethod @abstractmethod
def hsv(self) -> HSV: def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
"""Return the current HSV state of the bulb. """Return the current HSV state of the bulb.
:return: hue, saturation and value (degrees, %, %) :return: hue, saturation and value (degrees, %, %)
@ -137,12 +137,12 @@ class Light(Module, ABC):
@property @property
@abstractmethod @abstractmethod
def color_temp(self) -> int: def color_temp(self) -> Annotated[int, FeatureAttribute()]:
"""Whether the bulb supports color temperature changes.""" """Whether the bulb supports color temperature changes."""
@property @property
@abstractmethod @abstractmethod
def brightness(self) -> int: def brightness(self) -> Annotated[int, FeatureAttribute()]:
"""Return the current brightness in percentage.""" """Return the current brightness in percentage."""
@abstractmethod @abstractmethod
@ -153,7 +153,7 @@ class Light(Module, ABC):
value: int | None = None, value: int | None = None,
*, *,
transition: int | None = None, transition: int | None = None,
) -> dict: ) -> Annotated[dict, FeatureAttribute()]:
"""Set new HSV. """Set new HSV.
Note, transition is not supported and will be ignored. Note, transition is not supported and will be ignored.
@ -167,7 +167,7 @@ class Light(Module, ABC):
@abstractmethod @abstractmethod
async def set_color_temp( async def set_color_temp(
self, temp: int, *, brightness: int | None = None, transition: int | None = None self, temp: int, *, brightness: int | None = None, transition: int | None = None
) -> dict: ) -> Annotated[dict, FeatureAttribute()]:
"""Set the color temperature of the device in kelvin. """Set the color temperature of the device in kelvin.
Note, transition is not supported and will be ignored. Note, transition is not supported and will be ignored.
@ -179,7 +179,7 @@ class Light(Module, ABC):
@abstractmethod @abstractmethod
async def set_brightness( async def set_brightness(
self, brightness: int, *, transition: int | None = None self, brightness: int, *, transition: int | None = None
) -> dict: ) -> Annotated[dict, FeatureAttribute()]:
"""Set the brightness in percentage. """Set the brightness in percentage.
Note, transition is not supported and will be ignored. Note, transition is not supported and will be ignored.

View File

@ -14,9 +14,17 @@ Light, AutoOff, Firmware etc.
>>> print(dev.alias) >>> print(dev.alias)
Living Room Bulb Living Room Bulb
To see whether a device supports functionality check for the existence of the module: To see whether a device supports a group of functionality
check for the existence of the module:
>>> if light := dev.modules.get("Light"): >>> if light := dev.modules.get("Light"):
>>> print(light.brightness)
100
To see whether a device supports specific functionality, you can check whether the
module has that feature:
>>> if light.has_feature("hsv"):
>>> print(light.hsv) >>> print(light.hsv)
HSV(hue=0, saturation=100, value=100) HSV(hue=0, saturation=100, value=100)
@ -70,6 +78,9 @@ ModuleT = TypeVar("ModuleT", bound="Module")
class FeatureAttribute: class FeatureAttribute:
"""Class for annotating attributes bound to feature.""" """Class for annotating attributes bound to feature."""
def __repr__(self) -> str:
return "FeatureAttribute"
class Module(ABC): class Module(ABC):
"""Base class implemention for all modules. """Base class implemention for all modules.
@ -147,6 +158,11 @@ class Module(ABC):
self._module = module self._module = module
self._module_features: dict[str, Feature] = {} self._module_features: dict[str, Feature] = {}
@property
def _all_features(self) -> dict[str, Feature]:
"""Get the features for this module and any sub modules."""
return self._module_features
def has_feature(self, attribute: str | property | Callable) -> bool: def has_feature(self, attribute: str | property | Callable) -> bool:
"""Return True if the module attribute feature is supported.""" """Return True if the module attribute feature is supported."""
return bool(self.get_feature(attribute)) return bool(self.get_feature(attribute))
@ -247,7 +263,7 @@ def _get_bound_feature(
) )
check = {attribute_name, attribute_callable} check = {attribute_name, attribute_callable}
for feature in module._module_features.values(): for feature in module._all_features.values():
if (getter := feature.attribute_getter) and getter in check: if (getter := feature.attribute_getter) and getter in check:
return feature return feature

View File

@ -3,11 +3,13 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import asdict from dataclasses import asdict
from typing import Annotated
from ...exceptions import KasaException from ...exceptions import KasaException
from ...feature import Feature
from ...interfaces.light import HSV, ColorTempRange, LightState from ...interfaces.light import HSV, ColorTempRange, LightState
from ...interfaces.light import Light as LightInterface from ...interfaces.light import Light as LightInterface
from ...module import Module from ...module import FeatureAttribute, Module
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
@ -16,6 +18,18 @@ class Light(SmartModule, LightInterface):
_light_state: LightState _light_state: LightState
@property
def _all_features(self) -> dict[str, Feature]:
"""Get the features for this module and any sub modules."""
ret: dict[str, Feature] = {}
if brightness := self._device.modules.get(Module.Brightness):
ret.update(**brightness._module_features)
if color := self._device.modules.get(Module.Color):
ret.update(**color._module_features)
if temp := self._device.modules.get(Module.ColorTemperature):
ret.update(**temp._module_features)
return ret
def query(self) -> dict: def query(self) -> dict:
"""Query to execute during the update cycle.""" """Query to execute during the update cycle."""
return {} return {}
@ -47,7 +61,7 @@ class Light(SmartModule, LightInterface):
return self._device.modules[Module.ColorTemperature].valid_temperature_range return self._device.modules[Module.ColorTemperature].valid_temperature_range
@property @property
def hsv(self) -> HSV: def hsv(self) -> Annotated[HSV, FeatureAttribute()]:
"""Return the current HSV state of the bulb. """Return the current HSV state of the bulb.
:return: hue, saturation and value (degrees, %, %) :return: hue, saturation and value (degrees, %, %)
@ -58,7 +72,7 @@ class Light(SmartModule, LightInterface):
return self._device.modules[Module.Color].hsv return self._device.modules[Module.Color].hsv
@property @property
def color_temp(self) -> int: def color_temp(self) -> Annotated[int, FeatureAttribute()]:
"""Whether the bulb supports color temperature changes.""" """Whether the bulb supports color temperature changes."""
if not self.is_variable_color_temp: if not self.is_variable_color_temp:
raise KasaException("Bulb does not support colortemp.") raise KasaException("Bulb does not support colortemp.")
@ -66,7 +80,7 @@ class Light(SmartModule, LightInterface):
return self._device.modules[Module.ColorTemperature].color_temp return self._device.modules[Module.ColorTemperature].color_temp
@property @property
def brightness(self) -> int: def brightness(self) -> Annotated[int, FeatureAttribute()]:
"""Return the current brightness in percentage.""" """Return the current brightness in percentage."""
if not self.is_dimmable: # pragma: no cover if not self.is_dimmable: # pragma: no cover
raise KasaException("Bulb is not dimmable.") raise KasaException("Bulb is not dimmable.")
@ -80,7 +94,7 @@ class Light(SmartModule, LightInterface):
value: int | None = None, value: int | None = None,
*, *,
transition: int | None = None, transition: int | None = None,
) -> dict: ) -> Annotated[dict, FeatureAttribute()]:
"""Set new HSV. """Set new HSV.
Note, transition is not supported and will be ignored. Note, transition is not supported and will be ignored.
@ -97,7 +111,7 @@ class Light(SmartModule, LightInterface):
async def set_color_temp( async def set_color_temp(
self, temp: int, *, brightness: int | None = None, transition: int | None = None self, temp: int, *, brightness: int | None = None, transition: int | None = None
) -> dict: ) -> Annotated[dict, FeatureAttribute()]:
"""Set the color temperature of the device in kelvin. """Set the color temperature of the device in kelvin.
Note, transition is not supported and will be ignored. Note, transition is not supported and will be ignored.
@ -113,7 +127,7 @@ class Light(SmartModule, LightInterface):
async def set_brightness( async def set_brightness(
self, brightness: int, *, transition: int | None = None self, brightness: int, *, transition: int | None = None
) -> dict: ) -> Annotated[dict, FeatureAttribute()]:
"""Set the brightness in percentage. """Set the brightness in percentage.
Note, transition is not supported and will be ignored. Note, transition is not supported and will be ignored.

View File

@ -25,7 +25,13 @@ classifiers = [
[project.optional-dependencies] [project.optional-dependencies]
speedups = ["orjson>=3.9.1", "kasa-crypt>=0.2.0"] speedups = ["orjson>=3.9.1", "kasa-crypt>=0.2.0"]
docs = ["sphinx~=6.2", "sphinx_rtd_theme~=2.0", "sphinxcontrib-programoutput~=0.0", "myst-parser", "docutils>=0.17"] docs = [
"sphinx_rtd_theme~=2.0",
"sphinxcontrib-programoutput~=0.0",
"myst-parser",
"docutils>=0.17",
"sphinx>=7.4.7",
]
shell = ["ptpython", "rich"] shell = ["ptpython", "rich"]
[project.urls] [project.urls]

32
uv.lock
View File

@ -381,11 +381,11 @@ wheels = [
[[package]] [[package]]
name = "docutils" name = "docutils"
version = "0.19" version = "0.20.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/330ea8d383eb2ce973df34d1239b3b21e91cd8c865d21ff82902d952f91f/docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6", size = 2056383 } sdist = { url = "https://files.pythonhosted.org/packages/1f/53/a5da4f2c5739cf66290fac1431ee52aff6851c7c8ffd8264f13affd7bcdd/docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b", size = 2058365 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/93/69/e391bd51bc08ed9141ecd899a0ddb61ab6465309f1eb470905c0c8868081/docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc", size = 570472 }, { url = "https://files.pythonhosted.org/packages/26/87/f238c0670b94533ac0353a4e2a1a771a0cc73277b88bff23d3ae35a256c1/docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6", size = 572666 },
] ]
[[package]] [[package]]
@ -556,14 +556,14 @@ wheels = [
[[package]] [[package]]
name = "markdown-it-py" name = "markdown-it-py"
version = "2.2.0" version = "3.0.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "mdurl" }, { name = "mdurl" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/e4/c0/59bd6d0571986f72899288a95d9d6178d0eebd70b6650f1bb3f0da90f8f7/markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1", size = 67120 } sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/25/2d88e8feee8e055d015343f9b86e370a1ccbec546f2865c98397aaef24af/markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30", size = 84466 }, { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
] ]
[[package]] [[package]]
@ -628,14 +628,14 @@ wheels = [
[[package]] [[package]]
name = "mdit-py-plugins" name = "mdit-py-plugins"
version = "0.3.5" version = "0.4.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "markdown-it-py" }, { name = "markdown-it-py" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/49/e7/cc2720da8a32724b36d04c6dba5644154cdf883a1482b3bbb81959a642ed/mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a", size = 39871 } sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/4c/a9b222f045f98775034d243198212cbea36d3524c3ee1e8ab8c0346d6953/mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e", size = 52087 }, { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316 },
] ]
[[package]] [[package]]
@ -740,7 +740,7 @@ wheels = [
[[package]] [[package]]
name = "myst-parser" name = "myst-parser"
version = "1.0.0" version = "4.0.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "docutils" }, { name = "docutils" },
@ -750,9 +750,9 @@ dependencies = [
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "sphinx" }, { name = "sphinx" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/5f/69/fbddb50198c6b0901a981e72ae30f1b7769d2dfac88071f7df41c946d133/myst-parser-1.0.0.tar.gz", hash = "sha256:502845659313099542bd38a2ae62f01360e7dd4b1310f025dd014dfc0439cdae", size = 84224 } sdist = { url = "https://files.pythonhosted.org/packages/85/55/6d1741a1780e5e65038b74bce6689da15f620261c490c3511eb4c12bac4b/myst_parser-4.0.0.tar.gz", hash = "sha256:851c9dfb44e36e56d15d05e72f02b80da21a9e0d07cba96baf5e2d476bb91531", size = 93858 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/1c/1f/1621ef434ac5da26c30d31fcca6d588e3383344902941713640ba717fa87/myst_parser-1.0.0-py3-none-any.whl", hash = "sha256:69fb40a586c6fa68995e6521ac0a525793935db7e724ca9bac1d33be51be9a4c", size = 77312 }, { url = "https://files.pythonhosted.org/packages/ca/b4/b036f8fdb667587bb37df29dc6644681dd78b7a2a6321a34684b79412b28/myst_parser-4.0.0-py3-none-any.whl", hash = "sha256:b9317997552424448c6096c2558872fdb6f81d3ecb3a40ce84a7518798f3f28d", size = 84563 },
] ]
[[package]] [[package]]
@ -1143,7 +1143,7 @@ requires-dist = [
{ name = "orjson", marker = "extra == 'speedups'", specifier = ">=3.9.1" }, { name = "orjson", marker = "extra == 'speedups'", specifier = ">=3.9.1" },
{ name = "ptpython", marker = "extra == 'shell'" }, { name = "ptpython", marker = "extra == 'shell'" },
{ name = "rich", marker = "extra == 'shell'" }, { name = "rich", marker = "extra == 'shell'" },
{ name = "sphinx", marker = "extra == 'docs'", specifier = "~=6.2" }, { name = "sphinx", marker = "extra == 'docs'", specifier = ">=7.4.7" },
{ name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = "~=2.0" }, { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = "~=2.0" },
{ name = "sphinxcontrib-programoutput", marker = "extra == 'docs'", specifier = "~=0.0" }, { name = "sphinxcontrib-programoutput", marker = "extra == 'docs'", specifier = "~=0.0" },
{ name = "tzdata", marker = "platform_system == 'Windows'", specifier = ">=2024.2" }, { name = "tzdata", marker = "platform_system == 'Windows'", specifier = ">=2024.2" },
@ -1287,7 +1287,7 @@ wheels = [
[[package]] [[package]]
name = "sphinx" name = "sphinx"
version = "6.2.1" version = "7.4.7"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "alabaster" }, { name = "alabaster" },
@ -1307,9 +1307,9 @@ dependencies = [
{ name = "sphinxcontrib-qthelp" }, { name = "sphinxcontrib-qthelp" },
{ name = "sphinxcontrib-serializinghtml" }, { name = "sphinxcontrib-serializinghtml" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/0f/6d/392defcc95ca48daf62aecb89550143e97a4651275e62a3d7755efe35a3a/Sphinx-6.2.1.tar.gz", hash = "sha256:6d56a34697bb749ffa0152feafc4b19836c755d90a7c59b72bc7dfd371b9cc6b", size = 6681092 } sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/d8/45ba6097c39ba44d9f0e1462fb232e13ca4ddb5aea93a385dcfa964687da/sphinx-6.2.1-py3-none-any.whl", hash = "sha256:97787ff1fa3256a3eef9eda523a63dbf299f7b47e053cfcf684a1c2a8380c912", size = 3024615 }, { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624 },
] ]
[[package]] [[package]]