updating docs to pull descriptions from structured configs

config_dataclasses
Andrew Mirsky 2025-07-11 22:24:27 -04:00
rodzic 1ec555ca23
commit 1be3a7c848
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: A98E67635CDF2C39
38 zmienionych plików z 286 dodań i 124 usunięć

Wyświetl plik

@ -3,7 +3,7 @@ from dataclasses import dataclass, field, fields, replace, asdict
from enum import Enum, StrEnum from enum import Enum, StrEnum
import logging import logging
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, LiteralString, Literal
from amqtt.mqtt.constants import QOS_0 from amqtt.mqtt.constants import QOS_0
@ -34,7 +34,7 @@ class ListenerType(StrEnum):
class Dictable: class Dictable:
def __getitem__(self, key): def __getitem__(self, key):
return getattr(self, key) return self.get(key)
def get(self, name, default=None): def get(self, name, default=None):
if hasattr(self, name): if hasattr(self, name):
@ -43,34 +43,6 @@ class Dictable:
return default return default
raise ValueError(f"'{name}' is not defined") raise ValueError(f"'{name}' is not defined")
def copy(self):
return replace(self)
def update(self, other):
if not isinstance(other, self.__class__):
raise TypeError(f'must update with another {self.__class__.__name__}')
for f in fields(self):
if isinstance(getattr(self, f.name), Dictable):
setattr(self, f.name, getattr(self, f.name).update(getattr(other, f.name)))
if getattr(self, f.name) == f.default:
setattr(self, f.name, other[f.name])
# if not isinstance(other, self.__class__):
# raise TypeError(f'must update with another {self.__class__.__name__}')
#
# valids = { k:v for k,v in asdict(self).items() if v is not None }
# return replace(other, **valids)
def __ior__(self, other):
self.update(other)
return self
# if not isinstance(other, self.__class__):
# raise TypeError(f'must update with another {self.__class__.__name__}')
# for f in fields(self):
# if getattr(self, f.name) == f.default:
# setattr(self, f.name, other[f.name])
# return self
def __contains__(self, name): def __contains__(self, name):
return getattr(self, name, None) is not None return getattr(self, name, None) is not None
@ -80,10 +52,13 @@ class Dictable:
@dataclass @dataclass
class ListenerConfig(Dictable): class ListenerConfig:
type: ListenerType = ListenerType.TCP type: ListenerType = ListenerType.TCP
"""listener type: 'tcp' or 'ws'"""
bind: str | None = "0.0.0.0:1883" bind: str | None = "0.0.0.0:1883"
"""address and port for the listener to bind to"""
max_connections: int = 0 max_connections: int = 0
""""""
ssl: bool = False ssl: bool = False
cafile: str | Path | None = None cafile: str | Path | None = None
capath: str | Path | None = None capath: str | Path | None = None
@ -96,6 +71,15 @@ class ListenerConfig(Dictable):
if isinstance(getattr(self, fn), str): if isinstance(getattr(self, fn), str):
setattr(self, fn, Path(getattr(self, fn))) setattr(self, fn, Path(getattr(self, fn)))
def apply(self, other):
"""Apply the field from 'other', if 'self' field is default."""
if not isinstance(other, ListenerConfig):
msg = f'cannot apply {self.__class__.__name__} to {other.__class__.__name__}'
raise TypeError(msg)
for f in fields(self):
if getattr(self, f.name) == f.default:
setattr(self, f.name, other[f.name])
def default_listeners(): def default_listeners():
return { return {
'default': ListenerConfig() 'default': ListenerConfig()
@ -112,12 +96,20 @@ def default_broker_plugins():
@dataclass @dataclass
class BrokerConfig(Dictable): class BrokerConfig(Dictable):
listeners: dict[str, ListenerConfig] = field(default_factory=dict) """Structured configuration for a broker. Can be passed directly to `amqtt.broker.Broker` or created from a dictionary.
"""
listeners: dict[Literal['default'] | str, ListenerConfig] = field(default_factory=default_listeners)
"""Network of listeners used by the services."""
sys_interval: int | None = None sys_interval: int | None = None
"""Deprecated field to configure the `BrokerSysPlugin`"""
timeout_disconnect_delay: int | None = 0 timeout_disconnect_delay: int | None = 0
plugins: dict | list | None = None """Client disconnect timeout without a keep-alive."""
auth: dict[str, Any] | None = None auth: dict[str, Any] | None = None
"""Deprecated field used to config EntryPoint-loaded plugins."""
topic_check: dict[str, Any] | None = None topic_check: dict[str, Any] | None = None
"""Deprecated field used to config EntryPoint-loaded plugins."""
plugins: dict | list | None = None
"""A list of strings representing the modules and class name of `BasePlugin`, `BaseAuthPlugin` and `BaseTopicPlugins`."""
def __post__init__(self) -> None: def __post__init__(self) -> None:
if self.sys_interval is not None: if self.sys_interval is not None:
@ -130,7 +122,9 @@ class BrokerConfig(Dictable):
default_listener = self.listeners['default'] default_listener = self.listeners['default']
for listener_name, listener in self.listeners.items(): for listener_name, listener in self.listeners.items():
listener.update(default_listener) if listener_name == 'default':
continue
listener.apply(default_listener)
if isinstance(self.plugins, list): if isinstance(self.plugins, list):
_plugins = {} _plugins = {}

Wyświetl plik

@ -1,8 +0,0 @@
{% extends "base.html" %}
{% block outdated %}
You're not viewing the latest version.
<a href="{{ '../' ~ base_url }}">
<strong>Click here to go to latest.</strong>
</a>
{% endblock %}

Wyświetl plik

@ -3,6 +3,22 @@
This configuration structure is valid as a python dictionary passed to the `amqtt.broker.Broker` class's `__init__` method or This configuration structure is valid as a python dictionary passed to the `amqtt.broker.Broker` class's `__init__` method or
as a yaml formatted file passed to the `amqtt` script. as a yaml formatted file passed to the `amqtt` script.
::: amqtt.contexts.BrokerConfig
options:
heading_level: 3
extra:
class_style: "simple"
::: amqtt.contexts.ListenerConfig
#### Manual docs
### `listeners` *(list[dict[str, Any]])* ### `listeners` *(list[dict[str, Any]])*
Defines the network listeners used by the service. Items defined in the `default` listener will be Defines the network listeners used by the service. Items defined in the `default` listener will be

Wyświetl plik

@ -0,0 +1,13 @@
{% extends "_base/class.html.jinja" %}
{% block signature scoped %}
{% if config.extra.class_style != 'simple' %}
{{ super() }}
{% endif %}
{% endblock signature %}
{% block bases scoped %}
{% if config.extra.class_style != 'simple' %}
{{ super() }}
{% endif %}
{% endblock bases %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/attribute.html.jinja" %}
{% block logs scoped %}
<p style="color:red">attribute.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/backlinks.html.jinja" %}
{% block logs scoped %}
<p style="color:red">backlinks.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/children.html.jinja" %}
{% block logs scoped %}
<p style="color:red">children.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/class.html.jinja" %}
{% block logs scoped %}
<p style="color:red">class.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/docstring.html.jinja" %}
{% block logs scoped %}
<p style="color:red">docstring.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/docstring/admonition.html.jinja" %}
{% block logs scoped %}
<p style="color:red">docstring/admonition.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/docstring/attributes.html.jinja" %}
{% block logs scoped %}
<p style="color:red">docstring/attributes.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/docstring/classes.html.jinja" %}
{% block logs scoped %}
<p style="color:red">docstring/classes.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/docstring/examples.html.jinja" %}
{% block logs scoped %}
<p style="color:red">docstring/examples.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/docstring/functions.html.jinja" %}
{% block logs scoped %}
<p style="color:red">docstring/functions.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/docstring/modules.html.jinja" %}
{% block logs scoped %}
<p style="color:red">docstring/modules.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/docstring/other_parameters.html.jinja" %}
{% block logs scoped %}
<p style="color:red">docstring/other_parameters.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/docstring/parameters.html.jinja" %}
{% block logs scoped %}
<p style="color:red">docstring/parameters.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/docstring/raises.html.jinja" %}
{% block logs scoped %}
<p style="color:red">docstring/raises.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/docstring/receives.html.jinja" %}
{% block logs scoped %}
<p style="color:red">docstring/receives.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/docstring/returns.html.jinja" %}
{% block logs scoped %}
<p style="color:red">docstring/returns.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/docstring/warns.html.jinja" %}
{% block logs scoped %}
<p style="color:red">docstring/warns.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/docstring/yields.html.jinja" %}
{% block logs scoped %}
<p style="color:red">docstring/yields.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,8 @@
{% extends "_base/expression.html.jinja" %}
{% block logs scoped %}
<< expression.html.jinja >>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/function.html.jinja" %}
{% block logs scoped %}
<p style="color:red">function.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/labels.html.jinja" %}
{% block logs scoped %}
<p style="color:red">labels.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/language.html.jinja" %}
{% block logs scoped %}
<p style="color:red">language.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/languages/en.html.jinja" %}
{% block logs scoped %}
<p style="color:red">languages/en.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/languages/ja.html.jinja" %}
{% block logs scoped %}
<p style="color:red">languages/ja.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/languages/zh.html.jinja" %}
{% block logs scoped %}
<p style="color:red">languages/zh.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/module.html.jinja" %}
{% block logs scoped %}
<p style="color:red">module.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/signature.html.jinja" %}
{% block logs scoped %}
<< signature.html.jinja >>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/summary.html.jinja" %}
{% block logs scoped %}
<p style="color:red">summary.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/summary/attributes.html.jinja" %}
{% block logs scoped %}
<p style="color:red">summary/attributes.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/summary/classes.html.jinja" %}
{% block logs scoped %}
<p style="color:red">summary/classes.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/summary/functions.html.jinja" %}
{% block logs scoped %}
<p style="color:red">summary/functions.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -0,0 +1,7 @@
{% extends "_base/summary/modules.html.jinja" %}
{% block logs scoped %}
<p style="color:red">summary/modules.html.jinja</p>
{% endblock logs %}

Wyświetl plik

@ -127,10 +127,12 @@ plugins:
- coverage - coverage
- mkdocs-typer2 - mkdocs-typer2
- mkdocstrings: - mkdocstrings:
custom_templates: 'docs/templates'
handlers: handlers:
python: python:
paths: [amqtt] paths: [amqtt]
options: options:
annotation_path: "full"
docstring_options: docstring_options:
ignore_init_summary: true ignore_init_summary: true
docstring_section_style: list docstring_section_style: list
@ -148,6 +150,8 @@ plugins:
show_symbol_type_toc: true show_symbol_type_toc: true
signature_crossrefs: true signature_crossrefs: true
summary: true summary: true
extensions:
- git-revision-date-localized: - git-revision-date-localized:
enabled: !ENV [DEPLOY, false] enabled: !ENV [DEPLOY, false]
enable_creation_date: true enable_creation_date: true

Wyświetl plik

@ -10,88 +10,6 @@ from amqtt.contexts import BrokerConfig, ListenerConfig, Dictable
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@dataclass
class NestedData(Dictable):
nested_one: int | None = None
nested_two: str | None = None
nested_three: str | None = "empty option"
@dataclass
class MainData(Dictable):
main_one: str | None = None
main_two: str | None = 2
main_three: dict[str, NestedData] | None = field(default_factory=dict)
data_a = {
'main_one': 'value main one',
'main_three': {
'nested_key_one': {
'nested_one': 101,
'nested_two': 'value nested two'
}
}
}
data_b = {
'option_one': 'value ten'
}
data_c = {
'main_two': 'value main two',
'main_three': {
'nested_key_one': {
'nested_two': 'other nested two'
},
'nested_key_two': {
'nested_one': 202,
'nested_three': 'other nested three'
}
}
}
def test_mismatched_data():
with pytest.raises(UnexpectedDataError):
_ = from_dict(data_class=MainData, data=data_b, config=Config(cast=[StrEnum], strict=True))
def test_nested_data():
main_a = from_dict(data_class=MainData, data=data_a, config=Config(cast=[StrEnum], strict=True))
main_c = from_dict(data_class=MainData, data=data_c, config=Config(cast=[StrEnum], strict=True))
main_a.update(main_c)
assert isinstance(main_c, MainData)
assert isinstance(main_c, MainData)
assert isinstance(main_c.main_three, dict)
assert 'nested_key_one' in main_c.main_three
assert isinstance(main_c.main_three['nested_key_one'], NestedData)
assert 'nested_key_two' in main_c.main_three
assert main_c.main_three['nested_key_two'].nested_two == 'value nested two'
assert main_a.main_one == 'value main one'
assert main_a.main_two == 'value main two'
assert main_a.main_three['nested_key_one'].nested_two == 'other nested two'
assert main_a.main_three['nested_key_one'] == 'value twelve'
assert main_a.main_three['nested_key_one'] == 'value three'
data = {
"listeners": {
"default": {
"bind": "0.0.0.0:1883",
"max_connections": 50
},
"secure": {
"bind": "0.0.0.0:8883",
"cafile": "ca.key"
}
}
}
def _test_broker_config(): def _test_broker_config():
# Parse with dacite # Parse with dacite